@futdevpro/nts-dynamo 1.15.80 → 1.15.82
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dynamo/logs/cicd-pipeline/output.log +1903 -1903
- package/.dynamo/logs/cicd-pipeline/status.json +36 -36
- package/build/_services/base/archive-data.service.d.ts.map +1 -1
- package/build/_services/base/archive-data.service.js +8 -0
- package/build/_services/base/archive-data.service.js.map +1 -1
- package/build/_services/server/app.server.d.ts +21 -7
- package/build/_services/server/app.server.d.ts.map +1 -1
- package/build/_services/server/app.server.js +33 -17
- package/build/_services/server/app.server.js.map +1 -1
- package/package.json +1 -1
- package/src/_services/base/archive-data.service.spec.ts +13 -0
- package/src/_services/base/archive-data.service.ts +11 -3
- package/src/_services/server/app.server-retention.spec.ts +10 -10
- package/src/_services/server/app.server.ts +43 -19
|
@@ -3,7 +3,7 @@ import { DyNTS_App, DyNTS_TtlIndexAction, DyNTS_TtlIndexCollection, DyNTS_TtlInd
|
|
|
3
3
|
/**
|
|
4
4
|
* FR-258 / SR-2 — unit tests for the retention TTL-index reconciliation logic
|
|
5
5
|
* (`DyNTS_App.ensureRetentionTtlIndex`). Pure with respect to a mockable native-driver collection;
|
|
6
|
-
* no live MongoDB required. Covers create / no-op /
|
|
6
|
+
* no live MongoDB required. Covers create / no-op / differs(boot-safe, no rebuild) and edge cases.
|
|
7
7
|
*/
|
|
8
8
|
describe('| DyNTS_App.ensureRetentionTtlIndex (FR-258 / SR-2)', () => {
|
|
9
9
|
|
|
@@ -51,25 +51,25 @@ describe('| DyNTS_App.ensureRetentionTtlIndex (FR-258 / SR-2)', () => {
|
|
|
51
51
|
expect(m.dropped.length).toBe(0);
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
-
it('| existing index with a DIFFERENT ttl →
|
|
54
|
+
it('| existing index with a DIFFERENT ttl → BOOT-SAFE: left untouched (action "differs", NO drop/recreate)', async () => {
|
|
55
55
|
const m: MockColl = makeMockCollection([
|
|
56
56
|
{ name: '__created_1', key: { __created: 1 }, expireAfterSeconds: 999 },
|
|
57
57
|
]);
|
|
58
58
|
const action: DyNTS_TtlIndexAction = await DyNTS_App.ensureRetentionTtlIndex(m.coll, TTL_14D);
|
|
59
|
-
expect(action).toBe('
|
|
60
|
-
|
|
61
|
-
expect(m.
|
|
62
|
-
expect(m.created
|
|
59
|
+
expect(action).toBe('differs');
|
|
60
|
+
// Critical: never rebuild a live index at boot (heavy op on large collections → health-probe starvation).
|
|
61
|
+
expect(m.dropped.length).toBe(0);
|
|
62
|
+
expect(m.created.length).toBe(0);
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
-
it('| existing NON-TTL __created index (no expireAfterSeconds) →
|
|
65
|
+
it('| existing NON-TTL __created index (no expireAfterSeconds) → BOOT-SAFE: left untouched (action "differs")', async () => {
|
|
66
66
|
const m: MockColl = makeMockCollection([
|
|
67
67
|
{ name: '__created_1', key: { __created: 1 } },
|
|
68
68
|
]);
|
|
69
69
|
const action: DyNTS_TtlIndexAction = await DyNTS_App.ensureRetentionTtlIndex(m.coll, 604800);
|
|
70
|
-
expect(action).toBe('
|
|
71
|
-
expect(m.dropped).
|
|
72
|
-
expect(m.created.length).toBe(
|
|
70
|
+
expect(action).toBe('differs');
|
|
71
|
+
expect(m.dropped.length).toBe(0);
|
|
72
|
+
expect(m.created.length).toBe(0);
|
|
73
73
|
});
|
|
74
74
|
|
|
75
75
|
it('| ignores indexes on OTHER fields — only reconciles { __created: 1 }', async () => {
|
|
@@ -254,8 +254,17 @@ export interface DyNTS_TtlIndexCollection {
|
|
|
254
254
|
dropIndex?: (name: string) => Promise<unknown>;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
-
/**
|
|
258
|
-
|
|
257
|
+
/**
|
|
258
|
+
* FR-258 / SR-2 — outcome of {@link DyNTS_App.ensureRetentionTtlIndex}.
|
|
259
|
+
* - `created`: no `__created` TTL index existed → one was created.
|
|
260
|
+
* - `noop`: an index with the SAME TTL already existed → nothing done.
|
|
261
|
+
* - `differs`: an index exists with a DIFFERENT TTL (or a non-TTL `__created` index) → left UNTOUCHED,
|
|
262
|
+
* only a warning is emitted. We deliberately DO NOT drop+recreate at boot: rebuilding a TTL index on a
|
|
263
|
+
* multi-GB production collection is a heavy server-side op that can starve concurrent boot queries (e.g.
|
|
264
|
+
* the health-probe) and trip a fail-safe deploy revert. A deliberate retention CHANGE must be applied via
|
|
265
|
+
* maintenance, not silently at every startup.
|
|
266
|
+
*/
|
|
267
|
+
export type DyNTS_TtlIndexAction = 'created' | 'differs' | 'noop';
|
|
259
268
|
|
|
260
269
|
export abstract class DyNTS_App extends DyNTS_SingletonService {
|
|
261
270
|
|
|
@@ -953,10 +962,16 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
|
|
|
953
962
|
|
|
954
963
|
resolve();
|
|
955
964
|
|
|
956
|
-
// FR-258 / SR-2 — install declared-retention TTL indexes.
|
|
957
|
-
// fully non-fatal:
|
|
958
|
-
//
|
|
959
|
-
|
|
965
|
+
// FR-258 / SR-2 — install declared-retention TTL indexes. DEFERRED + fire-and-forget +
|
|
966
|
+
// fully non-fatal: a create-if-missing index op (and the `indexes()` probes) must NOT
|
|
967
|
+
// compete with boot-readiness. We delay it well past the deploy health-probe window so a
|
|
968
|
+
// cold boot is never slowed by it, then run it detached. `.unref()` keeps it from holding
|
|
969
|
+
// the event loop open. Any failure is logged but never crashes the app. (`'differs'` never
|
|
970
|
+
// rebuilds a live index — see ensureRetentionTtlIndex.)
|
|
971
|
+
const ttlInstallTimer: ReturnType<typeof setTimeout> = setTimeout((): void => {
|
|
972
|
+
void this.installRetentionTtlIndexes();
|
|
973
|
+
}, 30000);
|
|
974
|
+
if (typeof ttlInstallTimer.unref === 'function') { ttlInstallTimer.unref(); }
|
|
960
975
|
})
|
|
961
976
|
.on('error', (error): void => {
|
|
962
977
|
if (!this.systemControls.mongoose.started) {
|
|
@@ -1085,14 +1100,21 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
|
|
|
1085
1100
|
|
|
1086
1101
|
try {
|
|
1087
1102
|
const action: DyNTS_TtlIndexAction = await DyNTS_App.ensureRetentionTtlIndex(collection, ttlSeconds);
|
|
1088
|
-
if (action === 'created'
|
|
1103
|
+
if (action === 'created') {
|
|
1089
1104
|
ensured++;
|
|
1090
1105
|
if (this.logSetup) {
|
|
1091
1106
|
DyFM_Log.success(
|
|
1092
|
-
`[FR-258 retention] TTL index
|
|
1107
|
+
`[FR-258 retention] TTL index created on "${dataName}" — ` +
|
|
1093
1108
|
`${Math.round(ttlSeconds / 86400)}d (${ttlSeconds}s)`,
|
|
1094
1109
|
);
|
|
1095
1110
|
}
|
|
1111
|
+
} else if (action === 'differs') {
|
|
1112
|
+
// Live index has a different TTL — left untouched at boot (see ensureRetentionTtlIndex doc).
|
|
1113
|
+
DyFM_Log.warn(
|
|
1114
|
+
`[FR-258 retention] "${dataName}" already has a {__created} index with a DIFFERENT TTL than the ` +
|
|
1115
|
+
`declared ${Math.round(ttlSeconds / 86400)}d (${ttlSeconds}s) — left as-is (no boot-time rebuild). ` +
|
|
1116
|
+
`Apply the change via maintenance if intended.`,
|
|
1117
|
+
);
|
|
1096
1118
|
}
|
|
1097
1119
|
} catch (idxErr: unknown) {
|
|
1098
1120
|
DyFM_Log.warn(
|
|
@@ -1115,11 +1137,16 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
|
|
|
1115
1137
|
|
|
1116
1138
|
/**
|
|
1117
1139
|
* FR-258 / SR-2 — ensure a single `{ __created: 1 }` TTL index with the given `expireAfterSeconds`.
|
|
1118
|
-
* Extracted as a pure-ish static so it is unit-testable with a mock collection. Idempotent:
|
|
1119
|
-
* - no existing `__created` index → create it
|
|
1120
|
-
* - existing with the SAME TTL → no-op
|
|
1121
|
-
* - existing with a DIFFERENT TTL / non-TTL →
|
|
1122
|
-
*
|
|
1140
|
+
* Extracted as a pure-ish static so it is unit-testable with a mock collection. Idempotent + BOOT-SAFE:
|
|
1141
|
+
* - no existing `__created` index → create it → `'created'`
|
|
1142
|
+
* - existing with the SAME TTL → no-op → `'noop'`
|
|
1143
|
+
* - existing with a DIFFERENT TTL / non-TTL → leave it as-is → `'differs'` (warn only — NO drop+recreate)
|
|
1144
|
+
*
|
|
1145
|
+
* The `'differs'` case intentionally does NOT mutate the live index. Dropping + recreating a TTL index on a
|
|
1146
|
+
* large (multi-GB) production collection is a heavy server-side operation; doing it automatically inside the
|
|
1147
|
+
* startup path can starve the concurrent boot queries (including the deploy health-probe) and trigger a
|
|
1148
|
+
* fail-safe revert. Create-if-missing is the essential growth-prevention behaviour; a deliberate retention
|
|
1149
|
+
* CHANGE on an already-indexed collection is a maintenance action, not a silent every-boot rebuild.
|
|
1123
1150
|
*/
|
|
1124
1151
|
static async ensureRetentionTtlIndex(
|
|
1125
1152
|
collection: DyNTS_TtlIndexCollection,
|
|
@@ -1138,12 +1165,9 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
|
|
|
1138
1165
|
if (existing.expireAfterSeconds === ttlSeconds) {
|
|
1139
1166
|
return 'noop';
|
|
1140
1167
|
}
|
|
1141
|
-
// Retention
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
}
|
|
1145
|
-
await collection.createIndex({ __created: 1 }, { expireAfterSeconds: ttlSeconds });
|
|
1146
|
-
return 'updated';
|
|
1168
|
+
// Retention differs from the live index (or it is a non-TTL `__created` index). Leave it UNTOUCHED at
|
|
1169
|
+
// boot — only report it. Avoids a heavy drop+recreate competing with boot-readiness on a large collection.
|
|
1170
|
+
return 'differs';
|
|
1147
1171
|
}
|
|
1148
1172
|
|
|
1149
1173
|
/**
|