@powersync/service-core 1.20.5 → 1.22.0
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/CHANGELOG.md +66 -0
- package/dist/api/RouteAPI.d.ts +3 -3
- package/dist/api/diagnostics.d.ts +1 -1
- package/dist/api/diagnostics.js +19 -3
- package/dist/api/diagnostics.js.map +1 -1
- package/dist/auth/RemoteJWKSCollector.js +3 -2
- package/dist/auth/RemoteJWKSCollector.js.map +1 -1
- package/dist/entry/commands/teardown-action.js +1 -1
- package/dist/entry/commands/teardown-action.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/modules/AbstractModule.d.ts +1 -1
- package/dist/replication/AbstractReplicationJob.js +1 -1
- package/dist/replication/AbstractReplicationJob.js.map +1 -1
- package/dist/replication/AbstractReplicator.d.ts +6 -6
- package/dist/replication/AbstractReplicator.js +21 -21
- package/dist/replication/AbstractReplicator.js.map +1 -1
- package/dist/replication/RelationCache.d.ts +9 -2
- package/dist/replication/RelationCache.js +21 -2
- package/dist/replication/RelationCache.js.map +1 -1
- package/dist/routes/configure-fastify.js +3 -1
- package/dist/routes/configure-fastify.js.map +1 -1
- package/dist/routes/endpoints/admin.js +16 -8
- package/dist/routes/endpoints/admin.js.map +1 -1
- package/dist/routes/endpoints/checkpointing.js +1 -1
- package/dist/routes/endpoints/checkpointing.js.map +1 -1
- package/dist/routes/endpoints/socket-route.js +1 -1
- package/dist/routes/endpoints/socket-route.js.map +1 -1
- package/dist/routes/endpoints/sync-rules.js +8 -8
- package/dist/routes/endpoints/sync-rules.js.map +1 -1
- package/dist/routes/endpoints/sync-stream.js +2 -2
- package/dist/routes/endpoints/sync-stream.js.map +1 -1
- package/dist/routes/route-register.d.ts +2 -0
- package/dist/routes/route-register.js +65 -3
- package/dist/routes/route-register.js.map +1 -1
- package/dist/runner/teardown.js +4 -4
- package/dist/runner/teardown.js.map +1 -1
- package/dist/storage/BucketStorage.d.ts +9 -9
- package/dist/storage/BucketStorage.js +9 -9
- package/dist/storage/BucketStorageBatch.d.ts +29 -0
- package/dist/storage/BucketStorageBatch.js.map +1 -1
- package/dist/storage/BucketStorageFactory.d.ts +27 -18
- package/dist/storage/BucketStorageFactory.js +13 -12
- package/dist/storage/BucketStorageFactory.js.map +1 -1
- package/dist/storage/PersistedSyncRulesContent.d.ts +6 -4
- package/dist/storage/PersistedSyncRulesContent.js +15 -8
- package/dist/storage/PersistedSyncRulesContent.js.map +1 -1
- package/dist/storage/SourceEntity.d.ts +8 -1
- package/dist/storage/SourceTable.d.ts +32 -11
- package/dist/storage/SourceTable.js +41 -15
- package/dist/storage/SourceTable.js.map +1 -1
- package/dist/storage/StorageVersionConfig.d.ts +1 -1
- package/dist/storage/StorageVersionConfig.js +1 -1
- package/dist/storage/SyncRulesBucketStorage.d.ts +63 -18
- package/dist/storage/SyncRulesBucketStorage.js +14 -0
- package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
- package/dist/storage/WriteCheckpointAPI.d.ts +6 -6
- package/dist/storage/WriteCheckpointAPI.js +1 -1
- package/dist/storage/bson.d.ts +0 -1
- package/dist/storage/bson.js +0 -4
- package/dist/storage/bson.js.map +1 -1
- package/dist/sync/BucketChecksumState.d.ts +6 -9
- package/dist/sync/BucketChecksumState.js +117 -58
- package/dist/sync/BucketChecksumState.js.map +1 -1
- package/dist/sync/sync.d.ts +2 -2
- package/dist/sync/sync.js.map +1 -1
- package/dist/tracing/PerformanceTracer.d.ts +60 -0
- package/dist/tracing/PerformanceTracer.js +105 -0
- package/dist/tracing/PerformanceTracer.js.map +1 -0
- package/dist/tracing/TraceWriter.d.ts +22 -0
- package/dist/tracing/TraceWriter.js +63 -0
- package/dist/tracing/TraceWriter.js.map +1 -0
- package/dist/util/checkpointing.js +1 -1
- package/dist/util/config/compound-config-collector.d.ts +1 -1
- package/dist/util/config/compound-config-collector.js +2 -2
- package/dist/util/config/compound-config-collector.js.map +1 -1
- package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js +1 -1
- package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js.map +1 -1
- package/dist/util/env.js +1 -1
- package/dist/util/protocol-types.d.ts +1 -1
- package/dist/util/protocol-types.js +1 -1
- package/dist/util/util-index.d.ts +1 -0
- package/dist/util/util-index.js +1 -0
- package/dist/util/util-index.js.map +1 -1
- package/dist/util/utils.d.ts +5 -0
- package/dist/util/utils.js +7 -0
- package/dist/util/utils.js.map +1 -1
- package/package.json +11 -11
- package/src/api/RouteAPI.ts +3 -3
- package/src/api/diagnostics.ts +29 -6
- package/src/auth/RemoteJWKSCollector.ts +3 -1
- package/src/entry/commands/teardown-action.ts +1 -1
- package/src/index.ts +2 -0
- package/src/modules/AbstractModule.ts +1 -1
- package/src/replication/AbstractReplicationJob.ts +1 -1
- package/src/replication/AbstractReplicator.ts +23 -23
- package/src/replication/RelationCache.ts +23 -4
- package/src/routes/configure-fastify.ts +8 -1
- package/src/routes/endpoints/admin.ts +17 -8
- package/src/routes/endpoints/checkpointing.ts +1 -1
- package/src/routes/endpoints/socket-route.ts +1 -1
- package/src/routes/endpoints/sync-rules.ts +8 -8
- package/src/routes/endpoints/sync-stream.ts +2 -2
- package/src/routes/route-register.ts +73 -4
- package/src/runner/teardown.ts +4 -4
- package/src/storage/BucketStorage.ts +9 -9
- package/src/storage/BucketStorageBatch.ts +32 -0
- package/src/storage/BucketStorageFactory.ts +35 -23
- package/src/storage/PersistedSyncRulesContent.ts +20 -12
- package/src/storage/SourceEntity.ts +9 -1
- package/src/storage/SourceTable.ts +56 -22
- package/src/storage/StorageVersionConfig.ts +1 -1
- package/src/storage/SyncRulesBucketStorage.ts +74 -22
- package/src/storage/WriteCheckpointAPI.ts +6 -6
- package/src/storage/bson.ts +0 -5
- package/src/sync/BucketChecksumState.ts +142 -78
- package/src/sync/sync.ts +4 -4
- package/src/tracing/PerformanceTracer.ts +149 -0
- package/src/tracing/TraceWriter.ts +67 -0
- package/src/util/checkpointing.ts +1 -1
- package/src/util/config/compound-config-collector.ts +3 -3
- package/src/util/config/sync-rules/impl/filesystem-sync-rules-collector.ts +1 -1
- package/src/util/env.ts +1 -1
- package/src/util/protocol-types.ts +1 -1
- package/src/util/util-index.ts +1 -0
- package/src/util/utils.ts +8 -0
- package/test/src/auth.test.ts +120 -1
- package/test/src/diagnostics.test.ts +155 -0
- package/test/src/routes/error-handler.integration.test.ts +275 -0
- package/test/src/routes/stream.test.ts +15 -4
- package/test/src/storage/SourceTable.test.ts +89 -0
- package/test/src/sync/BucketChecksumState.test.ts +244 -80
- package/test/tsconfig.json +0 -1
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
8
|
-
"version": "1.
|
|
8
|
+
"version": "1.22.0",
|
|
9
9
|
"main": "dist/index.js",
|
|
10
10
|
"license": "FSL-1.1-ALv2",
|
|
11
11
|
"type": "module",
|
|
@@ -24,26 +24,26 @@
|
|
|
24
24
|
"ipaddr.js": "^2.1.0",
|
|
25
25
|
"ix": "^5.0.0",
|
|
26
26
|
"jose": "^4.15.1",
|
|
27
|
-
"lodash": "^4.
|
|
27
|
+
"lodash": "^4.18.1",
|
|
28
28
|
"lru-cache": "^10.2.2",
|
|
29
29
|
"negotiator": "^1.0.0",
|
|
30
30
|
"node-fetch": "^3.3.2",
|
|
31
31
|
"ts-codec": "^1.3.0",
|
|
32
32
|
"uri-js": "^4.4.1",
|
|
33
|
-
"uuid": "^
|
|
33
|
+
"uuid": "^14.0.0",
|
|
34
34
|
"winston": "^3.13.0",
|
|
35
|
-
"yaml": "^2.3
|
|
36
|
-
"@powersync/lib-services-framework": "0.9.
|
|
37
|
-
"@powersync/service-
|
|
38
|
-
"@powersync/service-
|
|
39
|
-
"@powersync/service-sync-rules": "0.
|
|
40
|
-
"@powersync/service-types": "0.15.
|
|
35
|
+
"yaml": "^2.8.3",
|
|
36
|
+
"@powersync/lib-services-framework": "0.9.5",
|
|
37
|
+
"@powersync/service-jsonbig": "0.17.13",
|
|
38
|
+
"@powersync/service-rsocket-router": "0.2.21",
|
|
39
|
+
"@powersync/service-sync-rules": "0.37.0",
|
|
40
|
+
"@powersync/service-types": "0.15.2"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/async": "^3.2.24",
|
|
44
|
-
"@types/negotiator": "^0.6.4",
|
|
45
44
|
"@types/lodash": "^4.17.5",
|
|
46
|
-
"
|
|
45
|
+
"@types/negotiator": "^0.6.4",
|
|
46
|
+
"fastify": "^5.8.5",
|
|
47
47
|
"fastify-plugin": "^5.0.1"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
package/src/api/RouteAPI.ts
CHANGED
|
@@ -43,7 +43,7 @@ export interface RouteAPI {
|
|
|
43
43
|
* Generates replication table information from a given pattern of tables.
|
|
44
44
|
*
|
|
45
45
|
* @param tablePatterns A set of table patterns which typically come from
|
|
46
|
-
* the tables listed in sync
|
|
46
|
+
* the tables listed in sync config definitions.
|
|
47
47
|
*
|
|
48
48
|
* @param sqlSyncRules
|
|
49
49
|
* @returns A result of all the tables and columns which should be replicated
|
|
@@ -76,7 +76,7 @@ export interface RouteAPI {
|
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
78
|
* @returns The schema for tables inside the connected database. This is typically
|
|
79
|
-
* used to validate sync
|
|
79
|
+
* used to validate sync config.
|
|
80
80
|
*/
|
|
81
81
|
getConnectionSchema(): Promise<types.DatabaseSchema[]>;
|
|
82
82
|
|
|
@@ -92,7 +92,7 @@ export interface RouteAPI {
|
|
|
92
92
|
shutdown(): Promise<void>;
|
|
93
93
|
|
|
94
94
|
/**
|
|
95
|
-
* Get the default schema (or database) when only a table name is specified in sync
|
|
95
|
+
* Get the default schema (or database) when only a table name is specified in sync config.
|
|
96
96
|
*/
|
|
97
97
|
getParseSyncRulesOptions(): ParseSyncRulesOptions;
|
|
98
98
|
}
|
package/src/api/diagnostics.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { logger } from '@powersync/lib-services-framework';
|
|
2
|
-
import { DEFAULT_TAG,
|
|
2
|
+
import { DEFAULT_TAG, SourceTableRef, SyncConfigWithErrors } from '@powersync/service-sync-rules';
|
|
3
3
|
import { ReplicationError, SyncRulesStatus, TableInfo } from '@powersync/service-types';
|
|
4
4
|
|
|
5
5
|
import * as storage from '../storage/storage-index.js';
|
|
@@ -8,7 +8,7 @@ import { RouteAPI, SlotWalBudgetInfo } from './RouteAPI.js';
|
|
|
8
8
|
|
|
9
9
|
export interface DiagnosticsOptions {
|
|
10
10
|
/**
|
|
11
|
-
* Include sync
|
|
11
|
+
* Include sync config content in response.
|
|
12
12
|
*/
|
|
13
13
|
include_content?: boolean;
|
|
14
14
|
|
|
@@ -46,7 +46,7 @@ export async function getSyncRulesStatus(
|
|
|
46
46
|
let persisted: storage.PersistedSyncRules;
|
|
47
47
|
try {
|
|
48
48
|
persisted = sync_rules.parsed(apiHandler.getParseSyncRulesOptions());
|
|
49
|
-
parsed = persisted.
|
|
49
|
+
parsed = persisted.syncConfigWithErrors;
|
|
50
50
|
} catch (e) {
|
|
51
51
|
return {
|
|
52
52
|
content: include_content ? sync_rules.sync_rules_content : undefined,
|
|
@@ -90,7 +90,7 @@ export async function getSyncRulesStatus(
|
|
|
90
90
|
logger.warn(`Unable to get replication lag`, e);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
if (apiHandler.getSlotWalBudget) {
|
|
93
|
+
if (apiHandler.getSlotWalBudget && sync_rules.slot_name) {
|
|
94
94
|
try {
|
|
95
95
|
slot_wal_budget = await apiHandler.getSlotWalBudget({
|
|
96
96
|
slotName: sync_rules.slot_name
|
|
@@ -116,7 +116,7 @@ export async function getSyncRulesStatus(
|
|
|
116
116
|
errors: [{ level: 'fatal', message: 'connection failed', ts: now }]
|
|
117
117
|
};
|
|
118
118
|
} else {
|
|
119
|
-
const source:
|
|
119
|
+
const source: SourceTableRef = {
|
|
120
120
|
connectionTag: tag,
|
|
121
121
|
schema: pattern.schema,
|
|
122
122
|
name: pattern.tablePattern
|
|
@@ -145,8 +145,31 @@ export async function getSyncRulesStatus(
|
|
|
145
145
|
}
|
|
146
146
|
errors.push(...syncRuleErrors.map((error) => syncConfigYamlErrorToReplicationError(error, now)));
|
|
147
147
|
|
|
148
|
+
if (
|
|
149
|
+
slot_wal_budget &&
|
|
150
|
+
slot_wal_budget.wal_status !== 'lost' &&
|
|
151
|
+
slot_wal_budget.safe_wal_size != null &&
|
|
152
|
+
slot_wal_budget.max_slot_wal_keep_size != null &&
|
|
153
|
+
slot_wal_budget.max_slot_wal_keep_size > 0
|
|
154
|
+
) {
|
|
155
|
+
const budgetPct = Math.max(
|
|
156
|
+
0,
|
|
157
|
+
Math.round((slot_wal_budget.safe_wal_size / slot_wal_budget.max_slot_wal_keep_size) * 100)
|
|
158
|
+
);
|
|
159
|
+
if (budgetPct <= 50) {
|
|
160
|
+
errors.push({
|
|
161
|
+
level: 'warning',
|
|
162
|
+
message:
|
|
163
|
+
`WAL budget is low: ${budgetPct}% remaining. ` +
|
|
164
|
+
`The replication slot may be invalidated if WAL consumption ` +
|
|
165
|
+
`continues at this rate. Consider increasing max_slot_wal_keep_size.`,
|
|
166
|
+
ts: now
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
148
171
|
if (live_status && status?.active) {
|
|
149
|
-
// Check replication lag for active
|
|
172
|
+
// Check replication lag for active replication stream.
|
|
150
173
|
// Right now we exclude mysql, since it we don't have consistent keepalives for it.
|
|
151
174
|
if (sync_rules.last_checkpoint_ts == null && sync_rules.last_keepalive_ts == null) {
|
|
152
175
|
errors.push({
|
|
@@ -5,6 +5,7 @@ import fetch from 'node-fetch';
|
|
|
5
5
|
import {
|
|
6
6
|
AuthorizationError,
|
|
7
7
|
ErrorCode,
|
|
8
|
+
hostnameFromSocketAddress,
|
|
8
9
|
LookupOptions,
|
|
9
10
|
makeHostnameLookupFunction,
|
|
10
11
|
ServiceAssertionError,
|
|
@@ -140,7 +141,8 @@ export class RemoteJWKSCollector implements KeyCollector {
|
|
|
140
141
|
*/
|
|
141
142
|
resolveAgent(): http.Agent | https.Agent {
|
|
142
143
|
const lookupOptions = this.options?.lookupOptions ?? { reject_ip_ranges: [] };
|
|
143
|
-
const
|
|
144
|
+
const hostname = hostnameFromSocketAddress(this.url.hostname);
|
|
145
|
+
const lookup = makeHostnameLookupFunction(hostname, lookupOptions);
|
|
144
146
|
|
|
145
147
|
const options: http.AgentOptions = {
|
|
146
148
|
lookup
|
|
@@ -13,7 +13,7 @@ export function registerTearDownAction(program: Command) {
|
|
|
13
13
|
|
|
14
14
|
return teardownCommand
|
|
15
15
|
.argument('[ack]', 'Type `TEARDOWN` to confirm teardown should occur')
|
|
16
|
-
.description('Terminate all replicating
|
|
16
|
+
.description('Terminate all replicating replication streams, clear remote configuration and remove all data')
|
|
17
17
|
.action(async (ack, options) => {
|
|
18
18
|
if (ack !== 'TEARDOWN') {
|
|
19
19
|
throw new ServiceError(ErrorCode.PSYNC_S0102, 'TEARDOWN was not acknowledged.');
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { ServiceContextContainer } from '../system/ServiceContext.js';
|
|
|
5
5
|
|
|
6
6
|
export interface TearDownOptions {
|
|
7
7
|
/**
|
|
8
|
-
* If required, tear down any configuration/state for the specific
|
|
8
|
+
* If required, tear down any configuration/state for the specific replication stream
|
|
9
9
|
*/
|
|
10
10
|
syncRules?: PersistedSyncRulesContent[];
|
|
11
11
|
}
|
|
@@ -54,7 +54,7 @@ export abstract class AbstractReplicationJob {
|
|
|
54
54
|
* Safely stop the replication process
|
|
55
55
|
*/
|
|
56
56
|
public async stop(): Promise<void> {
|
|
57
|
-
this.logger.info(`Stopping replication job
|
|
57
|
+
this.logger.info(`Stopping replication job`);
|
|
58
58
|
this.abortController.abort();
|
|
59
59
|
await this.isReplicatingPromise;
|
|
60
60
|
}
|
|
@@ -32,19 +32,19 @@ export interface AbstractReplicatorOptions {
|
|
|
32
32
|
/**
|
|
33
33
|
* A replicator manages the mechanics for replicating data from a data source to a storage bucket.
|
|
34
34
|
* This includes copying across the original data set and then keeping it in sync with the data source using Replication Jobs.
|
|
35
|
-
* It also handles any changes to the sync
|
|
35
|
+
* It also handles any changes to the sync config.
|
|
36
36
|
*/
|
|
37
37
|
export abstract class AbstractReplicator<T extends AbstractReplicationJob = AbstractReplicationJob> {
|
|
38
38
|
protected logger: winston.Logger;
|
|
39
39
|
private lockAlerted: boolean = false;
|
|
40
40
|
/**
|
|
41
|
-
* Map of replication jobs by
|
|
42
|
-
* transitioning to a new
|
|
41
|
+
* Map of replication jobs by replication stream id. Usually there is only one running job, but there could be two when
|
|
42
|
+
* transitioning to a new replication stream.
|
|
43
43
|
*/
|
|
44
44
|
private replicationJobs = new Map<number, T>();
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
* Map of
|
|
47
|
+
* Map of replciation stream ids to promises that are clearing the replication stream.
|
|
48
48
|
*
|
|
49
49
|
* We primarily do this to keep track of what we're currently clearing, but don't currently
|
|
50
50
|
* use the Promise value.
|
|
@@ -68,8 +68,8 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
68
68
|
abstract createJob(options: CreateJobOptions): T;
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
71
|
+
* Clean up any configuration or state for the specified replication stream on the datasource.
|
|
72
|
+
* Should be a no-op if the replication stream has already been cleared
|
|
73
73
|
*/
|
|
74
74
|
abstract cleanUp(syncRuleStorage: storage.SyncRulesBucketStorage): Promise<void>;
|
|
75
75
|
|
|
@@ -100,7 +100,7 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
100
100
|
public async start(): Promise<void> {
|
|
101
101
|
this.abortController = new AbortController();
|
|
102
102
|
this.runLoop().catch((e) => {
|
|
103
|
-
this.logger.error('
|
|
103
|
+
this.logger.error('Fatal replication error', e);
|
|
104
104
|
container.reporter.captureException(e);
|
|
105
105
|
setTimeout(() => {
|
|
106
106
|
process.exit(1);
|
|
@@ -135,9 +135,9 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
135
135
|
|
|
136
136
|
let configuredLock: storage.ReplicationLock | undefined = undefined;
|
|
137
137
|
if (syncRules != null) {
|
|
138
|
-
this.logger.info('Loaded sync
|
|
138
|
+
this.logger.info('Loaded sync config');
|
|
139
139
|
try {
|
|
140
|
-
// Configure new sync
|
|
140
|
+
// Configure new sync config, if they have changed.
|
|
141
141
|
// In that case, also immediately take out a lock, so that another process doesn't start replication on it.
|
|
142
142
|
|
|
143
143
|
const { lock } = await this.storage.configureSyncRules(
|
|
@@ -149,11 +149,11 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
149
149
|
} catch (e) {
|
|
150
150
|
// Log and re-raise to exit.
|
|
151
151
|
// Should only reach this due to validation errors if exit_on_error is true.
|
|
152
|
-
this.logger.error(`Failed to update sync
|
|
152
|
+
this.logger.error(`Failed to update sync config`, e);
|
|
153
153
|
throw e;
|
|
154
154
|
}
|
|
155
155
|
} else {
|
|
156
|
-
this.logger.info('No sync rules configured - configure via API');
|
|
156
|
+
this.logger.info('No sync streams or rules configured - configure via API');
|
|
157
157
|
}
|
|
158
158
|
while (!this.stopped) {
|
|
159
159
|
await container.probes.touch();
|
|
@@ -206,7 +206,7 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
206
206
|
// Remove from the list. Next refresh call will restart the job.
|
|
207
207
|
existingJobs.delete(syncRules.id);
|
|
208
208
|
} else {
|
|
209
|
-
// New sync
|
|
209
|
+
// New sync config was found (or resume after restart)
|
|
210
210
|
try {
|
|
211
211
|
let lock: storage.ReplicationLock;
|
|
212
212
|
if (configuredLock?.sync_rules_id == syncRules.id) {
|
|
@@ -229,15 +229,15 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
229
229
|
} catch (e) {
|
|
230
230
|
if (e?.errorData?.code === ErrorCode.PSYNC_S1003) {
|
|
231
231
|
if (!this.lockAlerted) {
|
|
232
|
-
|
|
232
|
+
syncRules.logger.info(`[${e.errorData.code}] ${e.errorData.description}`);
|
|
233
233
|
this.lockAlerted = true;
|
|
234
234
|
}
|
|
235
235
|
} else {
|
|
236
|
-
// Could be a sync
|
|
236
|
+
// Could be a sync config parse error,
|
|
237
237
|
// for example from stricter validation that was added.
|
|
238
238
|
// This will be retried every couple of seconds.
|
|
239
|
-
// When new (valid) sync
|
|
240
|
-
|
|
239
|
+
// When new (valid) sync config is deployed and processed, this one be disabled.
|
|
240
|
+
syncRules.logger.error('Failed to start replication for new sync config', e);
|
|
241
241
|
}
|
|
242
242
|
}
|
|
243
243
|
}
|
|
@@ -246,7 +246,7 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
246
246
|
this.replicationJobs = newJobs;
|
|
247
247
|
this.activeReplicationJob = activeJob;
|
|
248
248
|
|
|
249
|
-
// Stop any orphaned jobs that no longer have
|
|
249
|
+
// Stop any orphaned jobs that no longer have a replication stream.
|
|
250
250
|
// Termination happens below
|
|
251
251
|
for (let job of existingJobs.values()) {
|
|
252
252
|
// Old - stop and clean up
|
|
@@ -254,11 +254,11 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
254
254
|
await job.stop();
|
|
255
255
|
} catch (e) {
|
|
256
256
|
// This will be retried
|
|
257
|
-
|
|
257
|
+
job.storage.logger.warn('Failed to stop old replication job', e);
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
-
//
|
|
261
|
+
// Replication stream stopped previously, including by a different process.
|
|
262
262
|
const stopped = await this.storage.getStoppedSyncRules();
|
|
263
263
|
for (let syncRules of stopped) {
|
|
264
264
|
if (this.clearingJobs.has(syncRules.id)) {
|
|
@@ -268,11 +268,11 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
268
268
|
|
|
269
269
|
// We clear storage asynchronously.
|
|
270
270
|
// It is important to be able to continue running the refresh loop, otherwise we cannot
|
|
271
|
-
// retry locked
|
|
271
|
+
// retry locked replication stream, for example.
|
|
272
272
|
const syncRuleStorage = this.storage.getInstance(syncRules, { skipLifecycleHooks: true });
|
|
273
273
|
const promise = this.terminateSyncRules(syncRuleStorage)
|
|
274
274
|
.catch((e) => {
|
|
275
|
-
|
|
275
|
+
syncRuleStorage.logger.warn(`Failed clean up replication config`, e);
|
|
276
276
|
})
|
|
277
277
|
.finally(() => {
|
|
278
278
|
this.clearingJobs.delete(syncRules.id);
|
|
@@ -286,12 +286,12 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
protected async terminateSyncRules(syncRuleStorage: storage.SyncRulesBucketStorage) {
|
|
289
|
-
|
|
289
|
+
syncRuleStorage.logger.info(`Terminating replication stream...`);
|
|
290
290
|
// This deletes postgres replication slots - should complete quickly.
|
|
291
291
|
// It is safe to do before or after clearing the data in the storage.
|
|
292
292
|
await this.cleanUp(syncRuleStorage);
|
|
293
293
|
await syncRuleStorage.terminate({ signal: this.abortController?.signal, clearStorage: true });
|
|
294
|
-
|
|
294
|
+
syncRuleStorage.logger.info(`Successfully terminated replication stream`);
|
|
295
295
|
}
|
|
296
296
|
|
|
297
297
|
abstract testConnection(): Promise<ConnectionTestResult>;
|
|
@@ -1,24 +1,43 @@
|
|
|
1
1
|
import { SourceTable } from '../storage/SourceTable.js';
|
|
2
2
|
|
|
3
3
|
export class RelationCache<T> {
|
|
4
|
-
private cache = new Map<string | number, SourceTable>();
|
|
4
|
+
private cache = new Map<string | number, SourceTable[]>();
|
|
5
5
|
private idFunction: (item: T | SourceTable) => string | number;
|
|
6
6
|
|
|
7
7
|
constructor(idFunction: (item: T | SourceTable) => string | number) {
|
|
8
8
|
this.idFunction = idFunction;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Update a single table in-place - use when the snapshot status of the table has changed.
|
|
13
|
+
*/
|
|
11
14
|
update(table: SourceTable) {
|
|
12
15
|
const id = this.idFunction(table);
|
|
13
|
-
this.cache.
|
|
16
|
+
const existing = this.cache.get(id) ?? [];
|
|
17
|
+
const replacementIndex = existing.findIndex((candidate) => candidate.id == table.id);
|
|
18
|
+
if (replacementIndex == -1) {
|
|
19
|
+
this.cache.set(id, [...existing, table]);
|
|
20
|
+
} else {
|
|
21
|
+
const next = [...existing];
|
|
22
|
+
next[replacementIndex] = table;
|
|
23
|
+
this.cache.set(id, next);
|
|
24
|
+
}
|
|
14
25
|
}
|
|
15
26
|
|
|
16
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Set the full set of tables for a specific reference.
|
|
29
|
+
*/
|
|
30
|
+
updateAll(ref: T, tables: SourceTable[]) {
|
|
31
|
+
const id = this.idFunction(ref);
|
|
32
|
+
this.cache.set(id, tables);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getAll(source: T | SourceTable): SourceTable[] | undefined {
|
|
17
36
|
const id = this.idFunction(source);
|
|
18
37
|
return this.cache.get(id);
|
|
19
38
|
}
|
|
20
39
|
|
|
21
|
-
delete(source: T
|
|
40
|
+
delete(source: T): boolean {
|
|
22
41
|
const id = this.idFunction(source);
|
|
23
42
|
return this.cache.delete(id);
|
|
24
43
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type fastify from 'fastify';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
registerFastifyErrorHandler,
|
|
5
|
+
registerFastifyNotFoundHandler,
|
|
6
|
+
registerFastifyRoutes
|
|
7
|
+
} from './route-register.js';
|
|
4
8
|
|
|
5
9
|
import * as system from '../system/system-index.js';
|
|
6
10
|
|
|
@@ -66,6 +70,9 @@ export function configureFastifyServer(server: fastify.FastifyInstance, options:
|
|
|
66
70
|
};
|
|
67
71
|
};
|
|
68
72
|
|
|
73
|
+
// Set on the outer server so both child scopes inherit.
|
|
74
|
+
registerFastifyErrorHandler(server);
|
|
75
|
+
|
|
69
76
|
/**
|
|
70
77
|
* Fastify creates an encapsulated context for each `.register` call.
|
|
71
78
|
* Creating a separate context here to separate the concurrency limits for Admin APIs
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import * as sqlite from 'node:sqlite';
|
|
2
|
+
|
|
1
3
|
import { ErrorCode, errors, router, schema } from '@powersync/lib-services-framework';
|
|
2
|
-
import { SourceSchema, SqlSyncRules, StaticSchema } from '@powersync/service-sync-rules';
|
|
4
|
+
import { nodeSqlite, SourceSchema, SqlSyncRules, StaticSchema } from '@powersync/service-sync-rules';
|
|
3
5
|
import { internal_routes } from '@powersync/service-types';
|
|
4
6
|
|
|
5
7
|
import { DEFAULT_HYDRATION_STATE } from '@powersync/service-sync-rules';
|
|
@@ -119,7 +121,7 @@ export const reprocess = routeDefinition({
|
|
|
119
121
|
const apiHandler = service_context.routerEngine.getAPI();
|
|
120
122
|
const next = await activeBucketStorage.getNextSyncRules(apiHandler.getParseSyncRulesOptions());
|
|
121
123
|
if (next != null) {
|
|
122
|
-
throw new Error(`Busy processing sync
|
|
124
|
+
throw new Error(`Busy processing sync config - cannot reprocess`);
|
|
123
125
|
}
|
|
124
126
|
|
|
125
127
|
const active = await activeBucketStorage.getActiveSyncRules(apiHandler.getParseSyncRulesOptions());
|
|
@@ -127,13 +129,17 @@ export const reprocess = routeDefinition({
|
|
|
127
129
|
throw new errors.ServiceError({
|
|
128
130
|
status: 422,
|
|
129
131
|
code: ErrorCode.PSYNC_S4104,
|
|
130
|
-
description: 'No active sync
|
|
132
|
+
description: 'No active sync config'
|
|
131
133
|
});
|
|
132
134
|
}
|
|
133
135
|
|
|
136
|
+
// There are some differences between this and using asUpdateOptions():
|
|
137
|
+
// 1. This always re-parses the source YAML. If there are changes to the sync stream compiler, that can affect the sync plan.
|
|
138
|
+
// 2. If the source does not set the storage version, this will update it do the current version.
|
|
139
|
+
// We can consider tweaking this behavior in the future.
|
|
134
140
|
const new_rules = await activeBucketStorage.updateSyncRules(
|
|
135
|
-
storage.updateSyncRulesFromYaml(active.
|
|
136
|
-
//
|
|
141
|
+
storage.updateSyncRulesFromYaml(active.syncConfigWithErrors.config.content, {
|
|
142
|
+
// This sync config already passed validation. But if the config is not valid anymore due
|
|
137
143
|
// to a service change, we do want to report the error here.
|
|
138
144
|
validate: true
|
|
139
145
|
})
|
|
@@ -172,13 +178,16 @@ class FakeSyncRulesContentForValidation extends storage.PersistedSyncRulesConten
|
|
|
172
178
|
parsed(options: storage.ParseSyncRulesOptions): storage.PersistedSyncRules {
|
|
173
179
|
return {
|
|
174
180
|
...this,
|
|
175
|
-
|
|
181
|
+
syncConfigWithErrors: SqlSyncRules.fromYaml(this.sync_rules_content, {
|
|
176
182
|
...this.apiHandler.getParseSyncRulesOptions(),
|
|
177
183
|
schema: this.schema
|
|
178
184
|
}),
|
|
179
185
|
hydrationState: DEFAULT_HYDRATION_STATE,
|
|
180
|
-
|
|
181
|
-
return this.
|
|
186
|
+
hydratedSyncConfig() {
|
|
187
|
+
return this.syncConfigWithErrors.config.hydrate({
|
|
188
|
+
hydrationState: DEFAULT_HYDRATION_STATE,
|
|
189
|
+
sqlite: nodeSqlite(sqlite)
|
|
190
|
+
});
|
|
182
191
|
}
|
|
183
192
|
};
|
|
184
193
|
}
|
|
@@ -33,7 +33,7 @@ export const writeCheckpoint = routeDefinition({
|
|
|
33
33
|
const bucketStorage = await service_context.storageEngine.activeBucketStorage.getActiveStorage();
|
|
34
34
|
const cp = await bucketStorage?.getCheckpoint();
|
|
35
35
|
if (cp == null) {
|
|
36
|
-
throw new Error('No sync
|
|
36
|
+
throw new Error('No sync config available');
|
|
37
37
|
}
|
|
38
38
|
if (cp.lsn && cp.lsn >= head) {
|
|
39
39
|
logger.info(`Got write checkpoint: ${head} : ${cp.checkpoint}`);
|
|
@@ -77,7 +77,7 @@ export const syncStreamReactive: SocketRouteGenerator = (router) =>
|
|
|
77
77
|
new errors.ServiceError({
|
|
78
78
|
status: 500,
|
|
79
79
|
code: ErrorCode.PSYNC_S2302,
|
|
80
|
-
description: 'No sync
|
|
80
|
+
description: 'No sync config available'
|
|
81
81
|
})
|
|
82
82
|
);
|
|
83
83
|
responder.onComplete();
|
|
@@ -43,12 +43,12 @@ export const deploySyncRules = routeDefinition({
|
|
|
43
43
|
const { storageEngine } = service_context;
|
|
44
44
|
|
|
45
45
|
if (service_context.configuration.sync_rules.present) {
|
|
46
|
-
// If sync
|
|
46
|
+
// If sync config is configured via the service config, disable deploy via the API.
|
|
47
47
|
throw new errors.ServiceError({
|
|
48
48
|
status: 422,
|
|
49
49
|
code: ErrorCode.PSYNC_S4105,
|
|
50
|
-
description: 'Sync
|
|
51
|
-
details: '
|
|
50
|
+
description: 'Sync config API disabled',
|
|
51
|
+
details: 'Update sync config in the service configuration'
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
54
|
const content = payload.params.content;
|
|
@@ -65,7 +65,7 @@ export const deploySyncRules = routeDefinition({
|
|
|
65
65
|
throw new errors.ServiceError({
|
|
66
66
|
status: 422,
|
|
67
67
|
code: ErrorCode.PSYNC_R0001,
|
|
68
|
-
description: 'Sync
|
|
68
|
+
description: 'Sync config parsing failed',
|
|
69
69
|
details: e.message
|
|
70
70
|
});
|
|
71
71
|
}
|
|
@@ -115,7 +115,7 @@ export const currentSyncRules = routeDefinition({
|
|
|
115
115
|
throw new errors.ServiceError({
|
|
116
116
|
status: 422,
|
|
117
117
|
code: ErrorCode.PSYNC_S4104,
|
|
118
|
-
description: 'No active sync
|
|
118
|
+
description: 'No active sync config'
|
|
119
119
|
});
|
|
120
120
|
}
|
|
121
121
|
|
|
@@ -162,13 +162,13 @@ export const reprocessSyncRules = routeDefinition({
|
|
|
162
162
|
throw new errors.ServiceError({
|
|
163
163
|
status: 422,
|
|
164
164
|
code: ErrorCode.PSYNC_S4104,
|
|
165
|
-
description: 'No active sync
|
|
165
|
+
description: 'No active sync config'
|
|
166
166
|
});
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
const new_rules = await activeBucketStorage.updateSyncRules(
|
|
170
|
-
updateSyncRulesFromYaml(sync_rules.
|
|
171
|
-
//
|
|
170
|
+
updateSyncRulesFromYaml(sync_rules.syncConfigWithErrors.config.content, {
|
|
171
|
+
// This sync config already passed validation. But if the rules are not valid anymore due
|
|
172
172
|
// to a service change, we do want to report the error here.
|
|
173
173
|
validate: true
|
|
174
174
|
})
|
|
@@ -68,7 +68,7 @@ export const syncStreamed = routeDefinition({
|
|
|
68
68
|
throw new errors.ServiceError({
|
|
69
69
|
status: 500,
|
|
70
70
|
code: ErrorCode.PSYNC_S2302,
|
|
71
|
-
description: 'No sync
|
|
71
|
+
description: 'No sync config available'
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
74
|
|
|
@@ -121,7 +121,7 @@ export const syncStreamed = routeDefinition({
|
|
|
121
121
|
});
|
|
122
122
|
|
|
123
123
|
stream.on('end', () => {
|
|
124
|
-
// Auth failure or switch to new sync
|
|
124
|
+
// Auth failure or switch to new sync config
|
|
125
125
|
closeReason ??= 'service closing stream';
|
|
126
126
|
});
|
|
127
127
|
|