@hotmeshio/hotmesh 0.5.8 → 0.6.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/build/modules/enums.d.ts +6 -0
- package/build/modules/enums.js +7 -1
- package/build/package.json +1 -1
- package/build/services/engine/index.js +12 -2
- package/build/services/quorum/index.js +18 -1
- package/build/services/store/providers/postgres/postgres.js +3 -5
- package/build/services/store/providers/postgres/time-notify.d.ts +7 -0
- package/build/services/store/providers/postgres/time-notify.js +163 -0
- package/build/services/stream/providers/postgres/kvtables.js +81 -14
- package/build/services/sub/providers/postgres/postgres.js +28 -1
- package/build/types/quorum.d.ts +2 -0
- package/package.json +1 -1
package/build/modules/enums.d.ts
CHANGED
|
@@ -107,3 +107,9 @@ export declare const HMSH_GUID_SIZE: number;
|
|
|
107
107
|
* Default task queue name used when no task queue is specified
|
|
108
108
|
*/
|
|
109
109
|
export declare const DEFAULT_TASK_QUEUE = "default";
|
|
110
|
+
/**
|
|
111
|
+
* PostgreSQL NOTIFY payload limit. If a job message exceeds this size,
|
|
112
|
+
* a reference message is sent instead and the subscriber fetches via getState.
|
|
113
|
+
* PostgreSQL hard limit is 8000 bytes; default 7500 provides safety margin.
|
|
114
|
+
*/
|
|
115
|
+
export declare const HMSH_NOTIFY_PAYLOAD_LIMIT: number;
|
package/build/modules/enums.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DEFAULT_TASK_QUEUE = exports.HMSH_GUID_SIZE = exports.HMSH_SCOUT_INTERVAL_SECONDS = exports.HMSH_FIDELITY_SECONDS = exports.HMSH_EXPIRE_DURATION = exports.HMSH_XPENDING_COUNT = exports.HMSH_XCLAIM_COUNT = exports.HMSH_XCLAIM_DELAY_MS = exports.HMSH_BLOCK_TIME_MS = exports.HMSH_MEMFLOW_EXP_BACKOFF = exports.HMSH_MEMFLOW_MAX_INTERVAL = exports.HMSH_MEMFLOW_MAX_ATTEMPTS = exports.HMSH_GRADUATED_INTERVAL_MS = exports.HMSH_MAX_TIMEOUT_MS = exports.HMSH_MAX_RETRIES = exports.MAX_DELAY = exports.MAX_STREAM_RETRIES = exports.INITIAL_STREAM_BACKOFF = exports.MAX_STREAM_BACKOFF = exports.HMSH_EXPIRE_JOB_SECONDS = exports.HMSH_OTT_WAIT_TIME = exports.HMSH_DEPLOYMENT_PAUSE = exports.HMSH_DEPLOYMENT_DELAY = exports.HMSH_ACTIVATION_MAX_RETRY = exports.HMSH_QUORUM_DELAY_MS = exports.HMSH_QUORUM_ROLLCALL_CYCLES = exports.HMSH_STATUS_UNKNOWN = exports.HMSH_CODE_MEMFLOW_RETRYABLE = exports.HMSH_CODE_MEMFLOW_FATAL = exports.HMSH_CODE_MEMFLOW_MAXED = exports.HMSH_CODE_MEMFLOW_TIMEOUT = exports.HMSH_CODE_MEMFLOW_WAIT = exports.HMSH_CODE_MEMFLOW_PROXY = exports.HMSH_CODE_MEMFLOW_CHILD = exports.HMSH_CODE_MEMFLOW_ALL = exports.HMSH_CODE_MEMFLOW_SLEEP = exports.HMSH_CODE_UNACKED = exports.HMSH_CODE_TIMEOUT = exports.HMSH_CODE_UNKNOWN = exports.HMSH_CODE_INTERRUPT = exports.HMSH_CODE_NOTFOUND = exports.HMSH_CODE_PENDING = exports.HMSH_CODE_SUCCESS = exports.HMSH_SIGNAL_EXPIRE = exports.HMSH_TELEMETRY = exports.HMSH_LOGLEVEL = void 0;
|
|
3
|
+
exports.HMSH_NOTIFY_PAYLOAD_LIMIT = exports.DEFAULT_TASK_QUEUE = exports.HMSH_GUID_SIZE = exports.HMSH_SCOUT_INTERVAL_SECONDS = exports.HMSH_FIDELITY_SECONDS = exports.HMSH_EXPIRE_DURATION = exports.HMSH_XPENDING_COUNT = exports.HMSH_XCLAIM_COUNT = exports.HMSH_XCLAIM_DELAY_MS = exports.HMSH_BLOCK_TIME_MS = exports.HMSH_MEMFLOW_EXP_BACKOFF = exports.HMSH_MEMFLOW_MAX_INTERVAL = exports.HMSH_MEMFLOW_MAX_ATTEMPTS = exports.HMSH_GRADUATED_INTERVAL_MS = exports.HMSH_MAX_TIMEOUT_MS = exports.HMSH_MAX_RETRIES = exports.MAX_DELAY = exports.MAX_STREAM_RETRIES = exports.INITIAL_STREAM_BACKOFF = exports.MAX_STREAM_BACKOFF = exports.HMSH_EXPIRE_JOB_SECONDS = exports.HMSH_OTT_WAIT_TIME = exports.HMSH_DEPLOYMENT_PAUSE = exports.HMSH_DEPLOYMENT_DELAY = exports.HMSH_ACTIVATION_MAX_RETRY = exports.HMSH_QUORUM_DELAY_MS = exports.HMSH_QUORUM_ROLLCALL_CYCLES = exports.HMSH_STATUS_UNKNOWN = exports.HMSH_CODE_MEMFLOW_RETRYABLE = exports.HMSH_CODE_MEMFLOW_FATAL = exports.HMSH_CODE_MEMFLOW_MAXED = exports.HMSH_CODE_MEMFLOW_TIMEOUT = exports.HMSH_CODE_MEMFLOW_WAIT = exports.HMSH_CODE_MEMFLOW_PROXY = exports.HMSH_CODE_MEMFLOW_CHILD = exports.HMSH_CODE_MEMFLOW_ALL = exports.HMSH_CODE_MEMFLOW_SLEEP = exports.HMSH_CODE_UNACKED = exports.HMSH_CODE_TIMEOUT = exports.HMSH_CODE_UNKNOWN = exports.HMSH_CODE_INTERRUPT = exports.HMSH_CODE_NOTFOUND = exports.HMSH_CODE_PENDING = exports.HMSH_CODE_SUCCESS = exports.HMSH_SIGNAL_EXPIRE = exports.HMSH_TELEMETRY = exports.HMSH_LOGLEVEL = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* Determines the log level for the application. The default is 'info'.
|
|
6
6
|
*/
|
|
@@ -131,3 +131,9 @@ exports.HMSH_GUID_SIZE = Math.min(parseInt(process.env.HMSH_GUID_SIZE, 10) || 22
|
|
|
131
131
|
* Default task queue name used when no task queue is specified
|
|
132
132
|
*/
|
|
133
133
|
exports.DEFAULT_TASK_QUEUE = 'default';
|
|
134
|
+
/**
|
|
135
|
+
* PostgreSQL NOTIFY payload limit. If a job message exceeds this size,
|
|
136
|
+
* a reference message is sent instead and the subscriber fetches via getState.
|
|
137
|
+
* PostgreSQL hard limit is 8000 bytes; default 7500 provides safety margin.
|
|
138
|
+
*/
|
|
139
|
+
exports.HMSH_NOTIFY_PAYLOAD_LIMIT = parseInt(process.env.HMSH_NOTIFY_PAYLOAD_LIMIT, 10) || 7500;
|
package/build/package.json
CHANGED
|
@@ -547,7 +547,12 @@ class EngineService {
|
|
|
547
547
|
*/
|
|
548
548
|
async sub(topic, callback) {
|
|
549
549
|
const subscriptionCallback = async (topic, message) => {
|
|
550
|
-
|
|
550
|
+
let jobOutput = message.job;
|
|
551
|
+
// If _ref is true, payload was too large - fetch full job data via getState
|
|
552
|
+
if (message._ref && message.job?.metadata) {
|
|
553
|
+
jobOutput = await this.getState(message.job.metadata.tpc, message.job.metadata.jid);
|
|
554
|
+
}
|
|
555
|
+
callback(message.topic, jobOutput);
|
|
551
556
|
};
|
|
552
557
|
return await this.subscribe.subscribe(key_1.KeyType.QUORUM, subscriptionCallback, this.appId, topic);
|
|
553
558
|
}
|
|
@@ -562,7 +567,12 @@ class EngineService {
|
|
|
562
567
|
*/
|
|
563
568
|
async psub(wild, callback) {
|
|
564
569
|
const subscriptionCallback = async (topic, message) => {
|
|
565
|
-
|
|
570
|
+
let jobOutput = message.job;
|
|
571
|
+
// If _ref is true, payload was too large - fetch full job data via getState
|
|
572
|
+
if (message._ref && message.job?.metadata) {
|
|
573
|
+
jobOutput = await this.getState(message.job.metadata.tpc, message.job.metadata.jid);
|
|
574
|
+
}
|
|
575
|
+
callback(message.topic, jobOutput);
|
|
566
576
|
};
|
|
567
577
|
return await this.subscribe.psubscribe(key_1.KeyType.QUORUM, subscriptionCallback, this.appId, wild);
|
|
568
578
|
}
|
|
@@ -88,7 +88,24 @@ class QuorumService {
|
|
|
88
88
|
self.engine.processWebHooks();
|
|
89
89
|
}
|
|
90
90
|
else if (message.type === 'job') {
|
|
91
|
-
|
|
91
|
+
let jobOutput = message.job;
|
|
92
|
+
// If _ref is true, payload was too large - fetch full job data via getState
|
|
93
|
+
if (message._ref && message.job?.metadata) {
|
|
94
|
+
try {
|
|
95
|
+
jobOutput = await self.engine.getState(message.job.metadata.tpc, message.job.metadata.jid);
|
|
96
|
+
self.logger.debug('quorum-job-ref-resolved', {
|
|
97
|
+
jid: message.job.metadata.jid,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
self.logger.error('quorum-job-ref-error', {
|
|
102
|
+
jid: message.job.metadata.jid,
|
|
103
|
+
error: err,
|
|
104
|
+
});
|
|
105
|
+
return; // Can't route without job data
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
self.engine.routeToSubscribers(message.topic, jobOutput);
|
|
92
109
|
}
|
|
93
110
|
else if (message.type === 'cron') {
|
|
94
111
|
self.engine.processTimeHooks();
|
|
@@ -33,6 +33,7 @@ const cache_1 = require("../../cache");
|
|
|
33
33
|
const __1 = require("../..");
|
|
34
34
|
const kvsql_1 = require("./kvsql");
|
|
35
35
|
const kvtables_1 = require("./kvtables");
|
|
36
|
+
const time_notify_1 = require("./time-notify");
|
|
36
37
|
class PostgresStoreService extends __1.StoreService {
|
|
37
38
|
transact() {
|
|
38
39
|
return this.storeClient.transact();
|
|
@@ -1039,11 +1040,8 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
1039
1040
|
const schemaName = this.kvsql().safeName(appId);
|
|
1040
1041
|
const client = this.pgClient;
|
|
1041
1042
|
try {
|
|
1042
|
-
//
|
|
1043
|
-
const
|
|
1044
|
-
const path = await Promise.resolve().then(() => __importStar(require('path')));
|
|
1045
|
-
const sqlTemplate = fs.readFileSync(path.join(__dirname, 'time-notify.sql'), 'utf8');
|
|
1046
|
-
const sql = sqlTemplate.replace(/{schema}/g, schemaName);
|
|
1043
|
+
// Get the SQL with schema placeholder replaced
|
|
1044
|
+
const sql = (0, time_notify_1.getTimeNotifySql)(schemaName);
|
|
1047
1045
|
// Execute the entire SQL as one statement (functions contain $$ blocks with semicolons)
|
|
1048
1046
|
await client.query(sql);
|
|
1049
1047
|
this.logger.info('postgres-time-notifications-deployed', {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time-aware notification system for PostgreSQL
|
|
3
|
+
* This system minimizes polling by using LISTEN/NOTIFY for time-based task awakening
|
|
4
|
+
*
|
|
5
|
+
* Exported as a function that returns the SQL with schema placeholder replaced.
|
|
6
|
+
*/
|
|
7
|
+
export declare function getTimeNotifySql(schema: string): string;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getTimeNotifySql = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Time-aware notification system for PostgreSQL
|
|
6
|
+
* This system minimizes polling by using LISTEN/NOTIFY for time-based task awakening
|
|
7
|
+
*
|
|
8
|
+
* Exported as a function that returns the SQL with schema placeholder replaced.
|
|
9
|
+
*/
|
|
10
|
+
function getTimeNotifySql(schema) {
|
|
11
|
+
return `
|
|
12
|
+
-- Time-aware notification system for PostgreSQL
|
|
13
|
+
-- This system minimizes polling by using LISTEN/NOTIFY for time-based task awakening
|
|
14
|
+
|
|
15
|
+
-- Function to calculate the next awakening time from the sorted set
|
|
16
|
+
CREATE OR REPLACE FUNCTION ${schema}.get_next_awakening_time(app_key TEXT)
|
|
17
|
+
RETURNS TIMESTAMP WITH TIME ZONE AS $$
|
|
18
|
+
DECLARE
|
|
19
|
+
next_score DOUBLE PRECISION;
|
|
20
|
+
next_time TIMESTAMP WITH TIME ZONE;
|
|
21
|
+
BEGIN
|
|
22
|
+
-- Get the earliest (lowest score) entry from the time range ZSET
|
|
23
|
+
SELECT score INTO next_score
|
|
24
|
+
FROM ${schema}.task_schedules
|
|
25
|
+
WHERE key = app_key
|
|
26
|
+
AND (expiry IS NULL OR expiry > NOW())
|
|
27
|
+
ORDER BY score ASC
|
|
28
|
+
LIMIT 1;
|
|
29
|
+
|
|
30
|
+
IF next_score IS NULL THEN
|
|
31
|
+
RETURN NULL;
|
|
32
|
+
END IF;
|
|
33
|
+
|
|
34
|
+
-- Convert epoch milliseconds to timestamp
|
|
35
|
+
next_time := to_timestamp(next_score / 1000.0);
|
|
36
|
+
|
|
37
|
+
-- Only return if it's in the future
|
|
38
|
+
IF next_time > NOW() THEN
|
|
39
|
+
RETURN next_time;
|
|
40
|
+
END IF;
|
|
41
|
+
|
|
42
|
+
RETURN NULL;
|
|
43
|
+
END;
|
|
44
|
+
$$ LANGUAGE plpgsql;
|
|
45
|
+
|
|
46
|
+
-- Function to schedule a notification for the next awakening time
|
|
47
|
+
CREATE OR REPLACE FUNCTION ${schema}.schedule_time_notification(
|
|
48
|
+
app_id TEXT,
|
|
49
|
+
new_awakening_time TIMESTAMP WITH TIME ZONE DEFAULT NULL
|
|
50
|
+
)
|
|
51
|
+
RETURNS VOID AS $$
|
|
52
|
+
DECLARE
|
|
53
|
+
channel_name TEXT;
|
|
54
|
+
current_next_time TIMESTAMP WITH TIME ZONE;
|
|
55
|
+
app_key TEXT;
|
|
56
|
+
BEGIN
|
|
57
|
+
-- Build the time range key for this app
|
|
58
|
+
app_key := app_id || ':time_range';
|
|
59
|
+
channel_name := 'time_hooks_' || app_id;
|
|
60
|
+
|
|
61
|
+
-- Get the current next awakening time
|
|
62
|
+
current_next_time := ${schema}.get_next_awakening_time(app_key);
|
|
63
|
+
|
|
64
|
+
-- If we have a specific new awakening time, check if it's earlier
|
|
65
|
+
IF new_awakening_time IS NOT NULL THEN
|
|
66
|
+
IF current_next_time IS NULL OR new_awakening_time < current_next_time THEN
|
|
67
|
+
current_next_time := new_awakening_time;
|
|
68
|
+
END IF;
|
|
69
|
+
END IF;
|
|
70
|
+
|
|
71
|
+
-- If there's a next awakening time, schedule immediate notification
|
|
72
|
+
-- The application will handle the timing logic
|
|
73
|
+
IF current_next_time IS NOT NULL THEN
|
|
74
|
+
PERFORM pg_notify(channel_name, json_build_object(
|
|
75
|
+
'type', 'time_schedule_updated',
|
|
76
|
+
'app_id', app_id,
|
|
77
|
+
'next_awakening', extract(epoch from current_next_time) * 1000,
|
|
78
|
+
'updated_at', extract(epoch from NOW()) * 1000
|
|
79
|
+
)::text);
|
|
80
|
+
END IF;
|
|
81
|
+
END;
|
|
82
|
+
$$ LANGUAGE plpgsql;
|
|
83
|
+
|
|
84
|
+
-- Function to notify when time hooks are ready
|
|
85
|
+
CREATE OR REPLACE FUNCTION ${schema}.notify_time_hooks_ready(app_id TEXT)
|
|
86
|
+
RETURNS VOID AS $$
|
|
87
|
+
DECLARE
|
|
88
|
+
channel_name TEXT;
|
|
89
|
+
BEGIN
|
|
90
|
+
channel_name := 'time_hooks_' || app_id;
|
|
91
|
+
|
|
92
|
+
PERFORM pg_notify(channel_name, json_build_object(
|
|
93
|
+
'type', 'time_hooks_ready',
|
|
94
|
+
'app_id', app_id,
|
|
95
|
+
'ready_at', extract(epoch from NOW()) * 1000
|
|
96
|
+
)::text);
|
|
97
|
+
END;
|
|
98
|
+
$$ LANGUAGE plpgsql;
|
|
99
|
+
|
|
100
|
+
-- Trigger function for when time hooks are added/updated
|
|
101
|
+
CREATE OR REPLACE FUNCTION ${schema}.on_time_hook_change()
|
|
102
|
+
RETURNS TRIGGER AS $$
|
|
103
|
+
DECLARE
|
|
104
|
+
app_id_extracted TEXT;
|
|
105
|
+
awakening_time TIMESTAMP WITH TIME ZONE;
|
|
106
|
+
BEGIN
|
|
107
|
+
-- Extract app_id from the key (assumes format: app_id:time_range)
|
|
108
|
+
app_id_extracted := split_part(NEW.key, ':time_range', 1);
|
|
109
|
+
|
|
110
|
+
-- Convert the score (epoch milliseconds) to timestamp
|
|
111
|
+
awakening_time := to_timestamp(NEW.score / 1000.0);
|
|
112
|
+
|
|
113
|
+
-- Schedule notification for this new awakening time
|
|
114
|
+
PERFORM ${schema}.schedule_time_notification(app_id_extracted, awakening_time);
|
|
115
|
+
|
|
116
|
+
RETURN NEW;
|
|
117
|
+
END;
|
|
118
|
+
$$ LANGUAGE plpgsql;
|
|
119
|
+
|
|
120
|
+
-- Trigger function for when time hooks are removed
|
|
121
|
+
CREATE OR REPLACE FUNCTION ${schema}.on_time_hook_remove()
|
|
122
|
+
RETURNS TRIGGER AS $$
|
|
123
|
+
DECLARE
|
|
124
|
+
app_id_extracted TEXT;
|
|
125
|
+
BEGIN
|
|
126
|
+
-- Extract app_id from the key
|
|
127
|
+
app_id_extracted := split_part(OLD.key, ':time_range', 1);
|
|
128
|
+
|
|
129
|
+
-- Recalculate and notify about the schedule update
|
|
130
|
+
PERFORM ${schema}.schedule_time_notification(app_id_extracted);
|
|
131
|
+
|
|
132
|
+
RETURN OLD;
|
|
133
|
+
END;
|
|
134
|
+
$$ LANGUAGE plpgsql;
|
|
135
|
+
|
|
136
|
+
-- Create triggers on the sorted_set table for time hooks
|
|
137
|
+
-- Note: These will be created per app schema
|
|
138
|
+
-- Drop existing triggers first to avoid conflicts
|
|
139
|
+
DROP TRIGGER IF EXISTS trg_time_hook_insert ON ${schema}.task_schedules;
|
|
140
|
+
DROP TRIGGER IF EXISTS trg_time_hook_update ON ${schema}.task_schedules;
|
|
141
|
+
DROP TRIGGER IF EXISTS trg_time_hook_delete ON ${schema}.task_schedules;
|
|
142
|
+
|
|
143
|
+
-- Create new triggers
|
|
144
|
+
CREATE TRIGGER trg_time_hook_insert
|
|
145
|
+
AFTER INSERT ON ${schema}.task_schedules
|
|
146
|
+
FOR EACH ROW
|
|
147
|
+
WHEN (NEW.key LIKE '%:time_range')
|
|
148
|
+
EXECUTE FUNCTION ${schema}.on_time_hook_change();
|
|
149
|
+
|
|
150
|
+
CREATE TRIGGER trg_time_hook_update
|
|
151
|
+
AFTER UPDATE ON ${schema}.task_schedules
|
|
152
|
+
FOR EACH ROW
|
|
153
|
+
WHEN (NEW.key LIKE '%:time_range')
|
|
154
|
+
EXECUTE FUNCTION ${schema}.on_time_hook_change();
|
|
155
|
+
|
|
156
|
+
CREATE TRIGGER trg_time_hook_delete
|
|
157
|
+
AFTER DELETE ON ${schema}.task_schedules
|
|
158
|
+
FOR EACH ROW
|
|
159
|
+
WHEN (OLD.key LIKE '%:time_range')
|
|
160
|
+
EXECUTE FUNCTION ${schema}.on_time_hook_remove();
|
|
161
|
+
`;
|
|
162
|
+
}
|
|
163
|
+
exports.getTimeNotifySql = getTimeNotifySql;
|
|
@@ -1,25 +1,48 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getNotificationChannelName = exports.deploySchema = void 0;
|
|
4
|
+
const enums_1 = require("../../../../modules/enums");
|
|
5
|
+
const utils_1 = require("../../../../modules/utils");
|
|
4
6
|
async function deploySchema(streamClient, appId, logger) {
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const releaseClient =
|
|
7
|
+
const isPool = streamClient?.totalCount !== undefined &&
|
|
8
|
+
streamClient?.idleCount !== undefined;
|
|
9
|
+
const client = isPool ? await streamClient.connect() : streamClient;
|
|
10
|
+
const releaseClient = isPool;
|
|
9
11
|
try {
|
|
12
|
+
const schemaName = appId.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
13
|
+
const tableName = `${schemaName}.streams`;
|
|
14
|
+
// First, check if tables already exist (no lock needed)
|
|
15
|
+
const tablesExist = await checkIfTablesExist(client, schemaName, tableName);
|
|
16
|
+
if (tablesExist) {
|
|
17
|
+
// Tables already exist, no need to acquire lock or create tables
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
// Tables don't exist, need to acquire lock and create them
|
|
10
21
|
const lockId = getAdvisoryLockId(appId);
|
|
11
22
|
const lockResult = await client.query('SELECT pg_try_advisory_lock($1) AS locked', [lockId]);
|
|
12
23
|
if (lockResult.rows[0].locked) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
try {
|
|
25
|
+
await client.query('BEGIN');
|
|
26
|
+
// Double-check tables don't exist (race condition safety)
|
|
27
|
+
const tablesStillMissing = !(await checkIfTablesExist(client, schemaName, tableName));
|
|
28
|
+
if (tablesStillMissing) {
|
|
29
|
+
await createTables(client, schemaName, tableName);
|
|
30
|
+
await createNotificationTriggers(client, schemaName, tableName);
|
|
31
|
+
}
|
|
32
|
+
await client.query('COMMIT');
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
await client.query('SELECT pg_advisory_unlock($1)', [lockId]);
|
|
36
|
+
}
|
|
20
37
|
}
|
|
21
38
|
else {
|
|
22
|
-
|
|
39
|
+
// Release the client before waiting (if it's a pool connection)
|
|
40
|
+
if (releaseClient && client.release) {
|
|
41
|
+
await client.release();
|
|
42
|
+
}
|
|
43
|
+
// Wait for the deploy process to complete
|
|
44
|
+
await waitForTablesCreation(streamClient, lockId, schemaName, tableName, logger);
|
|
45
|
+
return; // Already released client, don't release again in finally
|
|
23
46
|
}
|
|
24
47
|
}
|
|
25
48
|
catch (error) {
|
|
@@ -27,8 +50,13 @@ async function deploySchema(streamClient, appId, logger) {
|
|
|
27
50
|
throw error;
|
|
28
51
|
}
|
|
29
52
|
finally {
|
|
30
|
-
if (releaseClient) {
|
|
31
|
-
|
|
53
|
+
if (releaseClient && client.release) {
|
|
54
|
+
try {
|
|
55
|
+
await client.release();
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Client may have been released already
|
|
59
|
+
}
|
|
32
60
|
}
|
|
33
61
|
}
|
|
34
62
|
}
|
|
@@ -44,6 +72,45 @@ function hashStringToInt(str) {
|
|
|
44
72
|
}
|
|
45
73
|
return Math.abs(hash);
|
|
46
74
|
}
|
|
75
|
+
async function checkIfTablesExist(client, schemaName, tableName) {
|
|
76
|
+
const result = await client.query(`SELECT to_regclass('${tableName}') AS t`);
|
|
77
|
+
return result.rows[0].t !== null;
|
|
78
|
+
}
|
|
79
|
+
async function waitForTablesCreation(streamClient, lockId, schemaName, tableName, logger) {
|
|
80
|
+
let retries = 0;
|
|
81
|
+
const maxRetries = Math.round(enums_1.HMSH_DEPLOYMENT_DELAY / enums_1.HMSH_DEPLOYMENT_PAUSE);
|
|
82
|
+
while (retries < maxRetries) {
|
|
83
|
+
await (0, utils_1.sleepFor)(enums_1.HMSH_DEPLOYMENT_PAUSE);
|
|
84
|
+
const isPool = streamClient?.totalCount !== undefined &&
|
|
85
|
+
streamClient?.idleCount !== undefined;
|
|
86
|
+
const client = isPool ? await streamClient.connect() : streamClient;
|
|
87
|
+
try {
|
|
88
|
+
// Check if tables exist directly (most efficient check)
|
|
89
|
+
const tablesExist = await checkIfTablesExist(client, schemaName, tableName);
|
|
90
|
+
if (tablesExist) {
|
|
91
|
+
// Tables now exist, deployment is complete
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// Fallback: check if the lock has been released (indicates completion)
|
|
95
|
+
const lockCheck = await client.query("SELECT NOT EXISTS (SELECT 1 FROM pg_locks WHERE locktype = 'advisory' AND objid = $1::bigint) AS unlocked", [lockId]);
|
|
96
|
+
if (lockCheck.rows[0].unlocked) {
|
|
97
|
+
// Lock has been released, tables should exist now
|
|
98
|
+
const tablesExistAfterLock = await checkIfTablesExist(client, schemaName, tableName);
|
|
99
|
+
if (tablesExistAfterLock) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
if (isPool && client.release) {
|
|
106
|
+
await client.release();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
retries++;
|
|
110
|
+
}
|
|
111
|
+
logger.error('stream-table-create-timeout', { schemaName, tableName });
|
|
112
|
+
throw new Error('Timeout waiting for stream table creation');
|
|
113
|
+
}
|
|
47
114
|
async function createTables(client, schemaName, tableName) {
|
|
48
115
|
await client.query(`CREATE SCHEMA IF NOT EXISTS ${schemaName};`);
|
|
49
116
|
// Main table creation with partitions
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.PostgresSubService = void 0;
|
|
7
7
|
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
const enums_1 = require("../../../../modules/enums");
|
|
8
9
|
const key_1 = require("../../../../modules/key");
|
|
9
10
|
const index_1 = require("../../index");
|
|
10
11
|
class PostgresSubService extends index_1.SubService {
|
|
@@ -180,8 +181,34 @@ class PostgresSubService extends index_1.SubService {
|
|
|
180
181
|
appId,
|
|
181
182
|
engineId: topic,
|
|
182
183
|
});
|
|
184
|
+
let messageToPublish = message;
|
|
185
|
+
let payload = JSON.stringify(message);
|
|
186
|
+
// PostgreSQL NOTIFY has a payload limit. If job message exceeds limit,
|
|
187
|
+
// send a reference message instead - subscriber will fetch via getState.
|
|
188
|
+
if (payload.length > enums_1.HMSH_NOTIFY_PAYLOAD_LIMIT &&
|
|
189
|
+
message.type === 'job' &&
|
|
190
|
+
message.job?.metadata) {
|
|
191
|
+
const { jid, tpc, app, js } = message.job.metadata;
|
|
192
|
+
messageToPublish = {
|
|
193
|
+
type: 'job',
|
|
194
|
+
topic: message.topic,
|
|
195
|
+
job: {
|
|
196
|
+
metadata: { jid, tpc, app, js },
|
|
197
|
+
data: null,
|
|
198
|
+
},
|
|
199
|
+
_ref: true,
|
|
200
|
+
};
|
|
201
|
+
payload = JSON.stringify(messageToPublish);
|
|
202
|
+
this.logger.debug('postgres-publish-ref', {
|
|
203
|
+
originalKey,
|
|
204
|
+
safeKey,
|
|
205
|
+
originalSize: JSON.stringify(message).length,
|
|
206
|
+
refSize: payload.length,
|
|
207
|
+
jid,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
183
210
|
// Publish the message using the safe topic
|
|
184
|
-
|
|
211
|
+
payload = payload.replace(/'/g, "''");
|
|
185
212
|
await this.storeClient.query(`NOTIFY "${safeKey}", '${payload}'`);
|
|
186
213
|
this.logger.debug(`postgres-publish`, { originalKey, safeKey });
|
|
187
214
|
return true;
|
package/build/types/quorum.d.ts
CHANGED
|
@@ -94,6 +94,8 @@ export interface JobMessage extends QuorumMessageBase {
|
|
|
94
94
|
entity?: string;
|
|
95
95
|
topic: string;
|
|
96
96
|
job: JobOutput;
|
|
97
|
+
/** if true, job.data is null due to payload size - subscriber should fetch via getState */
|
|
98
|
+
_ref?: boolean;
|
|
97
99
|
}
|
|
98
100
|
export interface ThrottleMessage extends QuorumMessageBase {
|
|
99
101
|
type: 'throttle';
|