Skip to content

Commit 73d674b

Browse files
committed
fix: skip import when singleton store is unsynced to prevent 10-min block
importOrganization (V1 and V2) already pre-checks the orgUid store's sync status before the blocking subscribe flow, but once the orgUid store is synced, subscribeToOrganization still reads the singleton (data-model-version) store — and that path uses a 10-minute wait loop on first import when the singleton has not yet propagated to the local datalayer. With N newly-discovered orgs in the default-org list, the sync-default-organizations* task could block for N x 10 min. After confirming the orgUid store is synced, read the org store once (fast, since it's synced) to discover the singleton id, subscribe to the singleton so it begins syncing, and pre-check its sync status. If the singleton is not yet synced, skip this import and let the next task run retry. Matches the skip-on-unsynced pattern used elsewhere.
1 parent 8faec6d commit 73d674b

3 files changed

Lines changed: 170 additions & 0 deletions

File tree

src/models/organizations/organizations.model.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -984,6 +984,59 @@ class Organization extends Model {
984984
);
985985
return;
986986
}
987+
988+
// Read the org store to discover the data-model-version (singleton) store
989+
// id, subscribe to it, and pre-check its sync status. Without this,
990+
// subscribeToOrganization would enter a 10-minute blocking wait loop on
991+
// the singleton store during first-time imports. Skip this import now
992+
// and let the next task run retry once the singleton has synced.
993+
let singletonStoreId;
994+
try {
995+
const orgStoreData = await datalayer.getSubscribedStoreData(
996+
orgUid,
997+
undefined,
998+
false,
999+
);
1000+
singletonStoreId = orgStoreData?.registryId;
1001+
} catch (error) {
1002+
logger.warn(
1003+
`[v1]: Could not read org store ${orgUid} to discover singleton id, skipping import: ${error.message}`,
1004+
);
1005+
return;
1006+
}
1007+
1008+
if (!singletonStoreId) {
1009+
logger.warn(
1010+
`[v1]: Org store ${orgUid} does not contain a data-model-version store id, skipping import.`,
1011+
);
1012+
return;
1013+
}
1014+
1015+
try {
1016+
await datalayer.subscribeToStoreOnDataLayer(singletonStoreId);
1017+
} catch (error) {
1018+
logger.warn(
1019+
`[v1]: Could not subscribe to singleton store ${singletonStoreId} for org ${orgUid}, skipping import: ${error.message}`,
1020+
);
1021+
return;
1022+
}
1023+
1024+
try {
1025+
const singletonSyncStatus = await datalayer.getDataLayerStoreSyncStatus(
1026+
singletonStoreId,
1027+
);
1028+
if (!isDlStoreSynced(singletonSyncStatus?.sync_status)) {
1029+
logger.info(
1030+
`[v1]: Skipping import of organization ${orgUid} - singleton store ${singletonStoreId} not yet synced. Will retry on next task run.`,
1031+
);
1032+
return;
1033+
}
1034+
} catch (error) {
1035+
logger.warn(
1036+
`[v1]: Could not check sync status for singleton store ${singletonStoreId}, skipping import: ${error.message}`,
1037+
);
1038+
return;
1039+
}
9871040
}
9881041

9891042
logger.verbose('[v1]: acquiring mutex to import organization');

src/models/v2/organizations-v2.model.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1623,6 +1623,60 @@ class OrganizationsV2 extends Model {
16231623
);
16241624
return;
16251625
}
1626+
1627+
// Read the org store to discover the data-model-version (singleton) store
1628+
// id, subscribe to it, and pre-check its sync status. Without this,
1629+
// subscribeToOrganization → getRegistryStoreIdFromSingleton would enter a
1630+
// 10-minute blocking wait loop on the singleton store during first-time
1631+
// imports. Skip this import now and let the next task run retry once
1632+
// the singleton has synced.
1633+
let singletonStoreId;
1634+
try {
1635+
const orgStoreData = await datalayer.getSubscribedStoreData(
1636+
orgUid,
1637+
undefined,
1638+
false,
1639+
);
1640+
singletonStoreId = orgStoreData?.registryId;
1641+
} catch (error) {
1642+
loggerV2.warn(
1643+
`[v2]: Could not read org store ${orgUid} to discover singleton id, skipping import: ${error.message}`,
1644+
);
1645+
return;
1646+
}
1647+
1648+
if (!singletonStoreId) {
1649+
loggerV2.warn(
1650+
`[v2]: Org store ${orgUid} does not contain a data-model-version store id, skipping import.`,
1651+
);
1652+
return;
1653+
}
1654+
1655+
try {
1656+
await datalayer.subscribeToStoreOnDataLayer(singletonStoreId);
1657+
} catch (error) {
1658+
loggerV2.warn(
1659+
`[v2]: Could not subscribe to singleton store ${singletonStoreId} for org ${orgUid}, skipping import: ${error.message}`,
1660+
);
1661+
return;
1662+
}
1663+
1664+
try {
1665+
const singletonSyncStatus = await datalayer.getDataLayerStoreSyncStatus(
1666+
singletonStoreId,
1667+
);
1668+
if (!isDlStoreSynced(singletonSyncStatus?.sync_status)) {
1669+
loggerV2.info(
1670+
`[v2]: Skipping import of organization ${orgUid} - singleton store ${singletonStoreId} not yet synced. Will retry on next task run.`,
1671+
);
1672+
return;
1673+
}
1674+
} catch (error) {
1675+
loggerV2.warn(
1676+
`[v2]: Could not check sync status for singleton store ${singletonStoreId}, skipping import: ${error.message}`,
1677+
);
1678+
return;
1679+
}
16261680
}
16271681

16281682
loggerV2.verbose('[v2]: Acquiring mutex to import organization');
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { expect } from 'chai';
2+
import { prepareV2Db } from '../../../src/database/v2/index.js';
3+
import { prepareDb } from '../../../src/database/index.js';
4+
import { OrganizationsV2 } from '../../../src/models/v2/index.js';
5+
import { Organization } from '../../../src/models/organizations/organizations.model.js';
6+
import TaskManager from '../../../src/tasks/index.js';
7+
8+
/**
9+
* importOrganization singleton-store pre-check — contract tests
10+
*
11+
* The new singleton pre-check is guarded by `if (!USE_SIMULATOR)`, so
12+
* simulator-mode tests cannot exercise it directly. Its correctness is
13+
* verified via production-mode live integration tests; here we only
14+
* assert that:
15+
*
16+
* 1. The method signature and availability are unchanged
17+
* 2. The simulator-mode behavior is unaffected
18+
*/
19+
describe('importOrganization singleton-store pre-check — contract', function () {
20+
this.timeout(10000);
21+
22+
before(async function () {
23+
await prepareDb();
24+
await prepareV2Db();
25+
TaskManager.stopAll();
26+
});
27+
28+
afterEach(async function () {
29+
await OrganizationsV2.destroy({ where: {} });
30+
});
31+
32+
describe('V1 Organization.importOrganization', function () {
33+
it('should be available as a static method', function () {
34+
expect(Organization.importOrganization).to.be.a('function');
35+
});
36+
37+
it('should accept orgUid and optional isHome arguments', function () {
38+
expect(Organization.importOrganization.length).to.equal(1);
39+
});
40+
});
41+
42+
describe('V2 OrganizationsV2.importOrganization', function () {
43+
it('should be available as a static method', function () {
44+
expect(OrganizationsV2.importOrganization).to.be.a('function');
45+
});
46+
47+
it('should accept orgUid and optional isHome arguments', function () {
48+
expect(OrganizationsV2.importOrganization.length).to.equal(1);
49+
});
50+
});
51+
52+
describe('Module structure — sync-default-organizations tasks', function () {
53+
it('V1 sync-default-organizations task should be importable', async function () {
54+
const job = (await import('../../../src/tasks/sync-default-organizations.js')).default;
55+
expect(job).to.exist;
56+
});
57+
58+
it('V2 sync-default-organizations task should be importable', async function () {
59+
const job = (await import('../../../src/tasks/sync-default-organizations-v2.js')).default;
60+
expect(job).to.exist;
61+
});
62+
});
63+
});

0 commit comments

Comments
 (0)