Skip to content

Commit d98198f

Browse files
committed
fix: centralize governance sync ownership in the scheduler
Adopt the orthogonal pieces of #1590 (which supersedes that PR's governance sync rewrite with #1600's simpler single-attempt-per-run design while keeping the #1600 5-minute cadence). - tasks/sync-default-organizations{,-v2}.js: remove the nested Governance.sync() trigger. Governance sync is now owned exclusively by sync-governance-body{,-v2}.js on its scheduler cadence; this task just reads whatever the local DB has and no-ops if orgList hasn't landed yet. Fixes a race where the task could block on governance sync before doing its own work. - utils/data-loaders{,-v2}.js: drop the 50-retry recursive wait loop in getDefaultOrganizationList{,V2}. On a fresh install where governance data hasn't been synced, return [] instead of retrying internally; the scheduler will re-run us on cadence. Also move the USE_SIMULATOR / USE_DEVELOPMENT_MODE destructure into the functions so config reloads aren't stale. - controllers/governance{,-v2}.controller.js: harden POST /governance/sync and POST /v2/governance/sync as fire-and-forget. The model sync() methods already return cleanly on any failure (subscribe- first pattern, scheduler retries on cadence); attach .catch defensively so future exceptions are logged instead of becoming unhandled rejections. Does NOT adopt #1590's two-tier bootstrap/steady cadence (keeping #1600's flat 300s default), the src/tasks/index.js scheduler refactor (not needed without the promotion machinery), or the importOrganization { allowLongWait } option (that belongs to #1603). Closes #1590 by folding in its compatible pieces.
1 parent fe4e591 commit d98198f

6 files changed

Lines changed: 121 additions & 159 deletions

File tree

src/controllers/governance.controller.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,13 @@ export const setGlossary = async (req, res) => {
245245
export const sync = async (req, res) => {
246246
try {
247247
await assertIfReadOnlyMode();
248-
Governance.sync();
248+
// Fire-and-forget: Governance.sync() returns cleanly on any failure
249+
// (subscribe-first pattern, scheduler retries on cadence), so a rejection
250+
// here is unexpected. Attach a .catch anyway so any future exception from
251+
// the sync coroutine is logged instead of becoming an unhandled rejection.
252+
void Governance.sync().catch((error) => {
253+
logger.error(`Governance sync request failed: ${error.message}`);
254+
});
249255
return res.json({
250256
message: 'Syncing Governance Body',
251257
success: true,

src/controllers/v2/governance-v2.controller.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,13 @@ export const setGlossary = async (req, res) => {
347347
export const sync = async (req, res) => {
348348
try {
349349
await assertV2IfReadOnlyMode();
350-
GovernanceV2.sync();
350+
// Fire-and-forget: GovernanceV2.sync() returns cleanly on any failure
351+
// (subscribe-first pattern, scheduler retries on cadence), so a rejection
352+
// here is unexpected. Attach a .catch anyway so any future exception from
353+
// the sync coroutine is logged instead of becoming an unhandled rejection.
354+
void GovernanceV2.sync().catch((error) => {
355+
loggerV2.error(`[v2]: Governance sync request failed: ${error.message}`);
356+
});
351357
return res.json({
352358
message: 'Syncing V2 Governance Body',
353359
success: true,

src/tasks/sync-default-organizations-v2.js

Lines changed: 8 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,51 +4,24 @@ import {
44
assertWalletIsSynced,
55
} from '../utils/data-assertions';
66
import { getDefaultOrganizationListV2 } from '../utils/v2-data-loaders.js';
7-
import { MetaV2, OrganizationsV2, GovernanceV2 } from '../models/v2/index.js';
7+
import { MetaV2, OrganizationsV2 } from '../models/v2/index.js';
88
import { loggerV2 } from '../config/logger.js';
9-
import { getConfig, getConfigV2 } from '../utils/config-loader.js';
10-
import _ from 'lodash';
9+
import { getConfig } from '../utils/config-loader.js';
1110

1211
const CONFIG = getConfig().APP;
13-
const CONFIG_V2 = getConfigV2();
1412

1513
const task = new Task('sync-default-organizations-v2', async () => {
1614
try {
1715
await assertDataLayerAvailable();
1816
await assertWalletIsSynced();
1917

2018
if (!CONFIG.USE_SIMULATOR) {
21-
// Check if governance data exists, and if not, trigger a governance sync
22-
// This ensures that once the wallet syncs, governance data will be synced
23-
// within the retry interval of this task (5 minutes) instead of waiting
24-
// for the governance sync task (24 hours)
25-
const governanceData = await GovernanceV2.findOne({
26-
where: { meta_key: 'orgList' },
27-
raw: true,
28-
});
29-
30-
if (!governanceData && CONFIG_V2.GOVERNANCE.GOVERNANCE_BODY_ID) {
31-
const v2HomeOrg = await OrganizationsV2.findOne({
32-
where: { is_home: true },
33-
raw: true,
34-
});
35-
if (!v2HomeOrg || v2HomeOrg.org_uid !== CONFIG_V2.GOVERNANCE.GOVERNANCE_BODY_ID) {
36-
loggerV2.info(
37-
'[v2]: Governance data not found, triggering governance sync before checking default organizations',
38-
);
39-
try {
40-
await GovernanceV2.sync();
41-
loggerV2.info('[v2]: Governance sync completed successfully');
42-
} catch (syncError) {
43-
loggerV2.warn(
44-
`[v2]: Governance sync failed, will retry on next run: ${syncError.message}`,
45-
);
46-
// Don't throw here - let the task continue and retry governance sync on next run
47-
// This allows the task to proceed if governance sync fails but data exists
48-
}
49-
}
50-
}
51-
19+
// Governance sync is owned by the scheduler
20+
// (src/tasks/sync-governance-body-v2.js). This task relies on whatever
21+
// governance data is already in the local DB; if none is present yet,
22+
// getDefaultOrganizationListV2() returns an empty list and we'll pick
23+
// up the orgs on the next run after the governance-sync task populates
24+
// them.
5225
const defaultOrgList = await getDefaultOrganizationListV2();
5326
const userDeletedOrgs = await MetaV2.getUserDeletedOrgUids();
5427

src/tasks/sync-default-organizations.js

Lines changed: 7 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@ import {
44
assertWalletIsSynced,
55
} from '../utils/data-assertions.js';
66
import { getDefaultOrganizationList } from '../utils/data-loaders.js';
7-
import { Meta, Organization, Governance } from '../models/index.js';
7+
import { Meta, Organization } from '../models/index.js';
88
import { logger } from '../config/logger.js';
99
import { getConfig } from '../utils/config-loader.js';
10-
import _ from 'lodash';
1110

1211
const CONFIG = getConfig();
1312

@@ -17,37 +16,12 @@ const task = new Task('sync-default-organizations', async () => {
1716
await assertWalletIsSynced();
1817

1918
if (!CONFIG.APP.USE_SIMULATOR) {
20-
// Check if governance data exists, and if not, trigger a governance sync
21-
// This ensures that once the wallet syncs, governance data will be synced
22-
// within the retry interval of this task (5 minutes) instead of waiting
23-
// for the governance sync task (24 hours)
24-
const governanceData = await Governance.findOne({
25-
where: { metaKey: 'orgList' },
26-
raw: true,
27-
});
28-
29-
if (!governanceData && CONFIG.GOVERNANCE.GOVERNANCE_BODY_ID) {
30-
const myOrganization = await Organization.getHomeOrg();
31-
if (
32-
_.get(myOrganization, 'orgUid', '') !==
33-
CONFIG.GOVERNANCE.GOVERNANCE_BODY_ID
34-
) {
35-
logger.info(
36-
'[v1]: Governance data not found, triggering governance sync before checking default organizations',
37-
);
38-
try {
39-
await Governance.sync();
40-
logger.info('[v1]: Governance sync completed successfully');
41-
} catch (syncError) {
42-
logger.warn(
43-
`[v1]: Governance sync failed, will retry on next run: ${syncError.message}`,
44-
);
45-
// Don't throw here - let the task continue and retry governance sync on next run
46-
// This allows the task to proceed if governance sync fails but data exists
47-
}
48-
}
49-
}
50-
19+
// Governance sync is owned by the scheduler
20+
// (src/tasks/sync-governance-body.js). This task relies on whatever
21+
// governance data is already in the local DB; if none is present yet,
22+
// getDefaultOrganizationList() returns an empty list and we'll pick up
23+
// the orgs on the next run after the governance-sync task populates
24+
// them.
5125
const defaultOrgRecords = await getDefaultOrganizationList();
5226
const userDeletedOrgs = await Meta.getUserDeletedOrgUids();
5327

src/utils/data-loaders.js

Lines changed: 38 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@ import PickListStub from '../models/governance/governance.stub.js';
55
import { getConfig } from '../utils/config-loader';
66
import { logger } from '../config/logger.js';
77

8-
const { USE_SIMULATOR, USE_DEVELOPMENT_MODE } = getConfig().APP;
9-
108
let downloadedPickList = {};
119
export const getPicklistValues = () => downloadedPickList;
1210

1311
export const pullPickListValues = async () => {
12+
const { USE_SIMULATOR, USE_DEVELOPMENT_MODE } = getConfig().APP;
1413
if (USE_SIMULATOR || USE_DEVELOPMENT_MODE) {
1514
downloadedPickList = PickListStub;
1615
} else {
@@ -37,42 +36,50 @@ export const pullPickListValues = async () => {
3736
return downloadedPickList;
3837
};
3938

40-
export const getDefaultOrganizationList = async (retryCount = 0) => {
41-
// need retry because on new install governance data may not have been synced yet
42-
let maxRetry = 50;
39+
export const getDefaultOrganizationList = async () => {
40+
// No in-function retry loop: the sync-default-organizations task calls this
41+
// on a scheduler cadence; on a fresh install where governance data has not
42+
// yet been synced, we return an empty list and let the scheduler re-run us.
43+
const { USE_SIMULATOR, USE_DEVELOPMENT_MODE } = getConfig().APP;
44+
if (USE_SIMULATOR || USE_DEVELOPMENT_MODE) {
45+
return [];
46+
}
4347

44-
try {
45-
if (USE_SIMULATOR || USE_DEVELOPMENT_MODE) {
46-
return [];
47-
} else {
48-
logger.debug(`[v1]: getting default organization list from governance data`);
49-
const governanceData = await Governance.findOne({
50-
where: { metaKey: 'orgList' },
51-
raw: true,
52-
});
48+
logger.debug(`[v1]: getting default organization list from governance data`);
49+
const governanceData = await Governance.findOne({
50+
where: { metaKey: 'orgList' },
51+
raw: true,
52+
});
5353

54-
if (governanceData) {
55-
const defaultOrgList = JSON.parse(
56-
_.get(governanceData, 'metaValue', null),
57-
);
58-
if (defaultOrgList && _.isArray(defaultOrgList)) {
59-
return defaultOrgList;
60-
}
61-
}
54+
if (governanceData) {
55+
const rawOrgList = _.get(governanceData, 'metaValue', null);
56+
if (rawOrgList == null) {
57+
throw new Error('[v1]: governance orgList record is missing metaValue');
58+
}
6259

63-
throw new Error(
64-
'governance data does not contain a default organization list',
65-
);
60+
let defaultOrgList;
61+
try {
62+
defaultOrgList = JSON.parse(rawOrgList);
63+
} catch (error) {
64+
throw new Error(`[v1]: cannot parse governance orgList JSON: ${error.message}`, {
65+
cause: error,
66+
});
6667
}
67-
} catch (error) {
68-
if (retryCount >= maxRetry) {
69-
throw error;
68+
69+
if (!_.isArray(defaultOrgList)) {
70+
throw new Error('[v1]: governance orgList must be an array');
7071
}
7172

72-
logger.warn(`[v1]: cannot get default org list. trying again Error: ${error}`);
73-
await new Promise((resolve) => setTimeout(resolve, 5000));
74-
return getDefaultOrganizationList(retryCount + 1);
73+
return defaultOrgList;
7574
}
75+
76+
const governanceDataExists = (await Governance.count()) > 0;
77+
logger.debug(
78+
governanceDataExists
79+
? '[v1]: Governance data exists but orgList is not available. Returning empty list. Will check again on next sync.'
80+
: '[v1]: Governance data has not been synced yet. Returning empty list and waiting for the next governance sync.',
81+
);
82+
return [];
7683
};
7784

7885
export const serverAvailable = async (server, port) => {

src/utils/v2-data-loaders.js

Lines changed: 54 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@ import PickListStub from '../models/governance/governance-v2.stub.js';
55
import { getConfig } from '../utils/config-loader';
66
import { loggerV2 } from '../config/logger.js';
77

8-
const { USE_SIMULATOR, USE_DEVELOPMENT_MODE } = getConfig().APP;
9-
108
let downloadedPickListV2 = {};
119
export const getPicklistValuesV2 = () => downloadedPickListV2;
1210

1311
export const pullPickListValuesV2 = async () => {
12+
const { USE_SIMULATOR, USE_DEVELOPMENT_MODE } = getConfig().APP;
1413
if (USE_SIMULATOR || USE_DEVELOPMENT_MODE) {
1514
downloadedPickListV2 = PickListStub;
1615
} else {
@@ -37,71 +36,68 @@ export const pullPickListValuesV2 = async () => {
3736
return downloadedPickListV2;
3837
};
3938

40-
export const getDefaultOrganizationListV2 = async (retryCount = 0) => {
41-
// need retry because on new install governance data may not have been synced yet
42-
const maxRetry = 50;
39+
export const getDefaultOrganizationListV2 = async () => {
40+
// No in-function retry loop: the sync-default-organizations-v2 task calls
41+
// this on a scheduler cadence; on a fresh install where governance data has
42+
// not yet been synced, we return an empty list and let the scheduler re-run
43+
// us.
44+
const { USE_SIMULATOR, USE_DEVELOPMENT_MODE } = getConfig().APP;
45+
if (USE_SIMULATOR || USE_DEVELOPMENT_MODE) {
46+
return [];
47+
}
4348

44-
try {
45-
if (USE_SIMULATOR || USE_DEVELOPMENT_MODE) {
46-
return [];
47-
} else {
48-
loggerV2.debug(`[v2]: getting default organization list from V2 governance data`);
49+
loggerV2.debug(`[v2]: getting default organization list from V2 governance data`);
4950

50-
// Always check for orgList first - this allows us to pick it up if it's added later
51-
const governanceData = await GovernanceV2.findOne({
52-
where: { meta_key: 'orgList' },
53-
raw: true,
54-
});
51+
// Always check for orgList first - this allows us to pick it up if it's added later
52+
const governanceData = await GovernanceV2.findOne({
53+
where: { meta_key: 'orgList' },
54+
raw: true,
55+
});
5556

56-
if (governanceData) {
57-
const defaultOrgList = JSON.parse(
58-
_.get(governanceData, 'meta_value', null),
59-
);
60-
if (defaultOrgList && _.isArray(defaultOrgList)) {
61-
loggerV2.debug(`[v2]: Found orgList with ${defaultOrgList.length} organizations`);
62-
return defaultOrgList;
63-
}
64-
}
57+
if (governanceData) {
58+
const rawOrgList = _.get(governanceData, 'meta_value', null);
59+
if (rawOrgList == null) {
60+
throw new Error('[v2]: governance orgList record is missing meta_value');
61+
}
6562

66-
// Check if governance data has been synced at all (check for pickList or glossary)
67-
// This check is only used to determine if we should retry waiting for initial sync
68-
const pickListData = await GovernanceV2.findOne({
69-
where: { meta_key: 'pickList' },
70-
raw: true,
71-
});
72-
const glossaryData = await GovernanceV2.findOne({
73-
where: { meta_key: 'glossary' },
74-
raw: true,
63+
let defaultOrgList;
64+
try {
65+
defaultOrgList = JSON.parse(rawOrgList);
66+
} catch (error) {
67+
throw new Error(`[v2]: cannot parse governance orgList JSON: ${error.message}`, {
68+
cause: error,
7569
});
76-
77-
if (pickListData || glossaryData) {
78-
// Governance data exists but orgList is missing - return empty array
79-
// The task will check again on its next run, so if orgList is added later, it will be picked up
80-
loggerV2.debug(
81-
'[v2]: Governance data exists but orgList is not available. Returning empty list. Will check again on next sync.',
82-
);
83-
return [];
84-
}
85-
86-
// Governance data hasn't been synced yet - retry to wait for initial sync
87-
throw new Error(
88-
'V2 governance data has not been synced yet. Waiting for initial governance sync.',
89-
);
9070
}
91-
} catch (error) {
92-
if (retryCount >= maxRetry) {
93-
// After max retries, if governance data still doesn't exist, return empty array
94-
// This prevents infinite retries if governance node doesn't provide orgList
95-
loggerV2.warn(
96-
`[v2]: Max retries reached for getting org list. Governance sync may not have completed yet. Returning empty list. Error: ${error.message}`,
97-
);
98-
return [];
71+
72+
if (!_.isArray(defaultOrgList)) {
73+
throw new Error('[v2]: governance orgList must be an array');
9974
}
10075

101-
loggerV2.warn(`[v2]: cannot get default org list from V2. trying again Error: ${error.message}`);
102-
await new Promise((resolve) => setTimeout(resolve, 5000));
103-
return getDefaultOrganizationListV2(retryCount + 1);
76+
loggerV2.debug(`[v2]: Found orgList with ${defaultOrgList.length} organizations`);
77+
return defaultOrgList;
78+
}
79+
80+
// Check if governance data has been synced at all (check for pickList or glossary)
81+
const pickListData = await GovernanceV2.findOne({
82+
where: { meta_key: 'pickList' },
83+
raw: true,
84+
});
85+
const glossaryData = await GovernanceV2.findOne({
86+
where: { meta_key: 'glossary' },
87+
raw: true,
88+
});
89+
90+
if (pickListData || glossaryData) {
91+
loggerV2.debug(
92+
'[v2]: Governance data exists but orgList is not available. Returning empty list. Will check again on next sync.',
93+
);
94+
return [];
10495
}
96+
97+
loggerV2.debug(
98+
'[v2]: Governance data has not been synced yet. Returning empty list and waiting for the next governance sync.',
99+
);
100+
return [];
105101
};
106102

107103
export const serverAvailable = async (server, port) => {

0 commit comments

Comments
 (0)