ti2 1.0.94 → 1.0.96

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.
@@ -45,28 +45,6 @@ describe('worker: Callback job handling', () => {
45
45
  }));
46
46
  });
47
47
 
48
- it('should include the correct structure for callback jobs', () => {
49
- // This test verifies the worker/index.js handles callback jobs correctly
50
- // by checking the code structure without actually running it
51
-
52
- // Read the worker code
53
- const workerCode = require('fs').readFileSync(require('path').resolve(__dirname, '../index.js'), 'utf8');
54
-
55
- // Verify the worker code includes handling for callback jobs
56
- expect(workerCode).toContain('if (type === \'callback\')');
57
-
58
- // Verify it extracts the necessary parameters
59
- expect(workerCode).toContain('callbackUrl,');
60
- expect(workerCode).toContain('request,');
61
- expect(workerCode).toContain('result,');
62
-
63
- // Verify it calls the sendCallback function
64
- expect(workerCode).toContain('sendCallback');
65
-
66
- // Verify it returns the expected result structure
67
- expect(workerCode).toContain('return { success: true }');
68
- });
69
-
70
48
  it('should use the callback module to send callbacks', async () => {
71
49
  // Get a reference to the mocked axios.post function
72
50
  const axios = require('axios');
@@ -0,0 +1,207 @@
1
+ // Mock throng to directly execute the worker function
2
+ jest.mock('throng', () => jest.fn(config => {
3
+ if (typeof config.worker === 'function') {
4
+ config.worker('test-worker-id-plugin', jest.fn());
5
+ } else {
6
+ console.error('Mocked throng (plugin test): config.worker is not a function or not provided.');
7
+ }
8
+ }));
9
+
10
+ // Mock the queue to avoid actual Redis operations and capture the handler
11
+ jest.mock('../queue', () => {
12
+ const original = jest.requireActual('../queue');
13
+ return {
14
+ ...original,
15
+ queue: {
16
+ ...original.queue,
17
+ process: jest.fn(), // Used to capture the job handler
18
+ },
19
+ addJob: jest.fn().mockResolvedValue('test-plugin-job-id'), // Mock addJob
20
+ saveResult: jest.fn().mockResolvedValue(undefined), // Mock saveResult
21
+ // listJobs, jobStatus etc. can be original or mocked if needed by other tests
22
+ };
23
+ });
24
+
25
+ // Mock bookingsControllerFactory to provide a mock for postProcess actions
26
+ const mockBookingsPostProcessAction = jest.fn();
27
+ jest.mock('../../controllers/bookings', () => {
28
+ return jest.fn(() => ({
29
+ // This action name must match what's in job.data.postProcess.action
30
+ $updateProductSearchCache: mockBookingsPostProcessAction,
31
+ // Add other methods if the worker's initialization of bookingsCtrl calls them
32
+ }));
33
+ });
34
+
35
+ // Import the mocked queue and the actual worker module
36
+ const { queue, addJob, saveResult } = require('../queue');
37
+ const actualWorkerModule = require('../index'); // Actual worker module
38
+ const testUtils = require('../../test/utils'); // For setting up plugins
39
+
40
+ describe('worker: Plugin job handling', () => {
41
+ let instantiatedPlugins; // Array of instantiated mock plugins from testUtils
42
+ let actualJobHandler; // The job processing function from the worker
43
+
44
+ beforeAll(async () => {
45
+ jest.spyOn(console, 'log').mockImplementation(() => {});
46
+ jest.spyOn(console, 'error').mockImplementation(() => {});
47
+
48
+
49
+ const utils = await testUtils({
50
+ plugins: ['mockPluginForWorker'], // Provide a name for our mock plugin
51
+ startServer: false, // Worker tests usually don't need a live server
52
+ worker: false,
53
+ });
54
+ instantiatedPlugins = utils.plugins; // These are already instantiated mock plugins
55
+
56
+ // Capture the job handler from queue.process
57
+ queue.process.mockImplementation((concurrency, handler) => {
58
+ if (typeof handler === 'function') {
59
+ actualJobHandler = handler;
60
+ } else if (typeof concurrency === 'function') { // Handle case where concurrency is omitted
61
+ actualJobHandler = concurrency;
62
+ } else {
63
+ throw new Error('queue.process mock in plugin test could not capture handler');
64
+ }
65
+ });
66
+
67
+ // Initialize the worker module. This should cause queue.process to be called.
68
+ await actualWorkerModule({ plugins: instantiatedPlugins });
69
+
70
+ if (!actualJobHandler) {
71
+ throw new Error('Job handler was not captured in plugin test. Worker might not have initialized queue processing correctly.');
72
+ }
73
+ });
74
+
75
+ beforeEach(() => {
76
+ jest.clearAllMocks(); // Clears all mocks, including plugin method spies and postProcess action
77
+ // Reset specific mocks if needed, e.g., return values for plugin methods
78
+ if (instantiatedPlugins && instantiatedPlugins.length > 0 && instantiatedPlugins[0].searchProducts) {
79
+ instantiatedPlugins[0].searchProducts.mockClear();
80
+ }
81
+ mockBookingsPostProcessAction.mockClear();
82
+ });
83
+
84
+ afterAll(() => {
85
+ jest.restoreAllMocks();
86
+ actualJobHandler = null;
87
+ });
88
+
89
+ it('should call postProcess action after successful plugin method execution', async () => {
90
+ const mockPluginName = 'mockPluginForWorker'; // Must match plugin name used in testUtils setup
91
+ const mockPluginMethod = 'searchProducts'; // Method to be called on the plugin
92
+ const mockPluginPayload = { query: 'test query' };
93
+ const mockPluginToken = { apiKey: 'test-api-key-for-plugin' };
94
+ const mockPluginResult = { products: [{ id: 'prod123', name: 'Test Product' }] };
95
+ const jobId = 'plugin-job-with-pp-001';
96
+
97
+ // Ensure the target mock plugin and its method exist
98
+ const targetPlugin = instantiatedPlugins.find(p => p.name === mockPluginName);
99
+ if (!targetPlugin || typeof targetPlugin[mockPluginMethod] !== 'function') {
100
+ throw new Error(`Mock plugin ${mockPluginName} or method ${mockPluginMethod} not found or not a function.`);
101
+ }
102
+ // Mock the plugin method's return value for this specific test
103
+ targetPlugin[mockPluginMethod].mockResolvedValue(mockPluginResult);
104
+
105
+ const jobDataWithPostProcess = {
106
+ type: 'plugin',
107
+ pluginName: mockPluginName,
108
+ method: mockPluginMethod,
109
+ payload: mockPluginPayload, // This is job.data.payload
110
+ token: mockPluginToken, // This is job.data.token
111
+ inTesting: true,
112
+ postProcess: {
113
+ controller: 'bookings',
114
+ action: '$updateProductSearchCache', // Must match the mocked action name in bookings mock
115
+ args: { staticArg1: 'value1', appKey: mockPluginName }, // Static args for postProcess
116
+ },
117
+ };
118
+
119
+ const mockJob = {
120
+ data: jobDataWithPostProcess,
121
+ id: jobId,
122
+ log: jest.fn(), // Bull's job.log
123
+ progress: jest.fn(), // Bull's job.progress
124
+ };
125
+
126
+ await actualJobHandler(mockJob);
127
+
128
+ // 1. Verify the plugin method was called correctly
129
+ expect(targetPlugin[mockPluginMethod]).toHaveBeenCalledTimes(1);
130
+ expect(targetPlugin[mockPluginMethod]).toHaveBeenCalledWith(expect.objectContaining({
131
+ ...mockPluginPayload, // job.data.payload is spread
132
+ token: mockPluginToken, // job.data.token is passed as 'token'
133
+ requestId: jobId,
134
+ // axios and typeDefsAndQueries are also injected by the worker
135
+ }));
136
+
137
+ // 2. Verify the postProcess action was called correctly
138
+ expect(mockBookingsPostProcessAction).toHaveBeenCalledTimes(1);
139
+ expect(mockBookingsPostProcessAction).toHaveBeenCalledWith(expect.objectContaining({
140
+ staticArg1: 'value1', // From postProcess.args
141
+ appKey: mockPluginName, // From postProcess.args
142
+ pluginResult: mockPluginResult, // The result from the plugin method
143
+ requestId: jobId,
144
+ }));
145
+
146
+ // 3. Verify saveResult was called (implicitly tests success path)
147
+ expect(saveResult).toHaveBeenCalledWith({ id: jobId, resultValue: mockPluginResult });
148
+ });
149
+
150
+ it('should still save result even if postProcess is not defined', async () => {
151
+ const mockPluginName = 'mockPluginForWorker';
152
+ const mockPluginMethod = 'searchProducts';
153
+ const mockPluginResult = { products: [{ id: 'prod456' }] };
154
+ const jobId = 'plugin-job-no-pp-002';
155
+
156
+ const targetPlugin = instantiatedPlugins.find(p => p.name === mockPluginName);
157
+ targetPlugin[mockPluginMethod].mockResolvedValue(mockPluginResult);
158
+
159
+ const jobDataNoPostProcess = {
160
+ type: 'plugin',
161
+ pluginName: mockPluginName,
162
+ method: mockPluginMethod,
163
+ payload: { query: 'another query' },
164
+ token: { apiKey: 'key-for-no-pp' },
165
+ inTesting: true,
166
+ // No postProcess field
167
+ };
168
+
169
+ const mockJob = { data: jobDataNoPostProcess, id: jobId, log: jest.fn(), progress: jest.fn() };
170
+ await actualJobHandler(mockJob);
171
+
172
+ expect(targetPlugin[mockPluginMethod]).toHaveBeenCalledTimes(1);
173
+ expect(mockBookingsPostProcessAction).not.toHaveBeenCalled();
174
+ expect(saveResult).toHaveBeenCalledWith({ id: jobId, resultValue: mockPluginResult });
175
+ });
176
+
177
+ it('should throw error and not call postProcess if plugin method fails', async () => {
178
+ const mockPluginName = 'mockPluginForWorker';
179
+ const mockPluginMethod = 'searchProducts';
180
+ const jobId = 'plugin-job-fail-003';
181
+ const pluginError = new Error('Plugin method failed');
182
+
183
+ const targetPlugin = instantiatedPlugins.find(p => p.name === mockPluginName);
184
+ targetPlugin[mockPluginMethod].mockRejectedValue(pluginError);
185
+
186
+ const jobDataWithPpForFailure = {
187
+ type: 'plugin',
188
+ pluginName: mockPluginName,
189
+ method: mockPluginMethod,
190
+ payload: {},
191
+ token: {},
192
+ inTesting: true,
193
+ postProcess: {
194
+ controller: 'bookings',
195
+ action: '$updateProductSearchCache',
196
+ args: { appKey: mockPluginName },
197
+ },
198
+ };
199
+ const mockJob = { data: jobDataWithPpForFailure, id: jobId, log: jest.fn(), progress: jest.fn() };
200
+
201
+ await expect(actualJobHandler(mockJob)).rejects.toThrow(pluginError.message);
202
+
203
+ expect(targetPlugin[mockPluginMethod]).toHaveBeenCalledTimes(1);
204
+ expect(mockBookingsPostProcessAction).not.toHaveBeenCalled();
205
+ expect(saveResult).not.toHaveBeenCalled(); // saveResult should not be called on failure
206
+ });
207
+ });
package/worker/index.js CHANGED
@@ -2,126 +2,209 @@ const throng = require('throng');
2
2
  require('util').inspect.defaultOptions.depth = null;
3
3
  const R = require('ramda');
4
4
  const { queue, saveResult } = require('./queue');
5
+ const bookingsControllerFactory = require('../controllers/bookings');
6
+ const mainAxios = require('axios'); // Generic axios instance for direct calls
5
7
 
6
8
  const workers = process.env.WEB_CONCURRENCY || 2;
7
9
  const maxJobsPerWorker = process.env.MAX_JOBS_PER_WORKER || 1;
8
10
 
9
- const worker = ({ plugins: pluginsParam }) => (id, disconnect) => {
10
- console.log(`Started worker ${id}`);
11
+ const worker = ({ plugins: pluginsParam }) => { // pluginsParam are instantiated plugins
12
+ // Initialize bookingsCtrl once per worker instance using the passed instantiated plugins.
13
+ const bookingsCtrl = bookingsControllerFactory(pluginsParam);
11
14
 
12
- const bye = () => {
13
- console.log(`Worker ${id} exiting`);
14
- disconnect();
15
- process.exit(0);
16
- };
15
+ return (id, disconnect) => {
16
+ console.log(`Started worker ${id}`);
17
+
18
+ const bye = () => {
19
+ console.log(`Worker ${id} exiting`);
20
+ disconnect();
21
+ // Graceful shutdown of the queue connection
22
+ queue.close().then(() => process.exit(0)).catch(() => process.exit(1));
23
+ };
17
24
 
18
- process.on('SIGTERM', bye);
19
- process.on('SIGUSR2', bye);
20
- process.on('SIGINT', bye);
21
-
22
- queue.process(maxJobsPerWorker, async job => {
23
- const {
24
- data: {
25
- type = 'plugin',
26
- pluginName,
27
- method,
28
- userId,
29
- inTesting,
30
- payload = {}
31
- },
32
- id: jobId,
33
- data: params,
34
- } = job;
35
- if (type === 'api') {
25
+ process.on('SIGTERM', bye);
26
+ process.on('SIGUSR2', bye);
27
+ process.on('SIGINT', bye); // Good for local development
28
+
29
+ queue.process(maxJobsPerWorker, async job => {
36
30
  const {
37
- method,
38
- url,
39
- payload,
40
- headers,
41
- } = params;
42
-
43
- let code, result, server;
44
- const http = require('http');
45
- const axios = require('axios');
31
+ data, // Full job.data object
32
+ id: jobId,
33
+ } = job;
34
+
35
+ // Destructure from data for clarity and to match existing patterns
36
+ const {
37
+ type = 'plugin', // Default job type if not specified
38
+ pluginName, // Name of the plugin context for the job
39
+ inTesting, // Flag if running in a testing environment
40
+ payload: jobDataPayload, // This is job.data.payload (e.g., { methodName: '...', args: {...} } or other plugin args)
41
+ method: jobDataMethod, // This is job.data.method (e.g., 'searchProducts' for generic plugin calls)
42
+ // Note: 'token' can also be a top-level property in data (data.token)
43
+ } = data;
44
+
45
+ let resultValue; // This will be set by all job type handlers
46
+ const jobLog = msg => console.log(`job ${jobId} (type: ${type}, plugin: ${pluginName || 'N/A'}) > `, msg);
47
+ const jobErr = msg => console.error(`job ${jobId} (type: ${type}, plugin: ${pluginName || 'N/A'}) > `, msg);
48
+ jobLog(`started`);
49
+
46
50
  try {
47
- const app = await require('../index')({
48
- pluginsInstantiated: pluginsParam,
49
- startServer: false,
50
- worker: false,
51
- });
52
-
53
- // Manually start server on random port
54
- server = http.createServer(app);
55
- await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
56
- const { address, port } = server.address();
57
- const baseURL = `http://${address}:${port}`;
58
- console.log(`Worker internal server for job ${jobId} listening on: ${baseURL}`);
59
-
60
- // Use axios to make the request
61
- const response = await axios({
62
- method: method.toLowerCase(),
63
- url: `${baseURL}${url}`,
64
- data: R.omit(['backgroundJob'], payload),
65
- headers: R.omit(['content-length'], headers),
66
- validateStatus: () => true, // Prevent axios from throwing on non-2xx status
67
- });
68
-
69
- code = response.status;
70
- result = response.data;
71
- console.log(`Worker internal request for job ${jobId} completed with code ${code}`);
72
- } catch (error) {
73
- // Handle errors during app init or axios request
74
- console.error(`Worker internal request error for job ${jobId}:`, error.message);
75
- // If axios error has response, use its status
76
- code = R.pathOr(500, ['response', 'status'], error);
77
- result = R.pathOr({ message: error.message }, ['response', 'data'], error);
78
- } finally {
79
- // Ensure server is closed if it was created
80
- if (server) {
81
- await new Promise(resolve => server.close(resolve));
82
- console.log(`Worker internal server for job ${jobId} closed.`);
51
+ if (type === 'api') {
52
+ // params is job.data, so data.method, data.url etc. are correct here
53
+ jobLog(`running method ${data.method} ${data.url}`);
54
+ let code, result, server;
55
+ const http = require('http');
56
+ const axios = require('axios'); // Local axios for API call type
57
+ try {
58
+ const app = await require('../index')({
59
+ pluginsInstantiated: pluginsParam, // Pass the already instantiated plugins
60
+ startServer: false,
61
+ worker: false,
62
+ });
63
+
64
+ server = http.createServer(app);
65
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
66
+ const { address, port } = server.address();
67
+ const baseURL = `http://${address}:${port}`;
68
+ jobLog(`Worker internal server for job ${jobId} listening on: ${baseURL}`);
69
+
70
+ // data.payload is the payload for the API call job type
71
+ const { token: jwtTokenForHeader, ...requestBodyForWorker } = data.payload || {};
72
+
73
+ const requestHeaders = {
74
+ ...R.omit(['content-length', 'host', 'connection', 'accept-encoding'], data.headers),
75
+ ...(jwtTokenForHeader ? { Authorization: `Bearer ${jwtTokenForHeader}` } : {})
76
+ };
77
+
78
+ const response = await axios({
79
+ method: data.method.toLowerCase(),
80
+ url: `${baseURL}${data.url}`,
81
+ data: R.omit(['backgroundJob'], requestBodyForWorker),
82
+ headers: requestHeaders,
83
+ validateStatus: () => true,
84
+ });
85
+
86
+ code = response.status;
87
+ result = response.data;
88
+ jobLog(`Worker internal request for job ${jobId} completed with code ${code}`);
89
+ } catch (error) {
90
+ jobErr(`Worker internal request error for job ${jobId}: ${error.message}`);
91
+ code = R.pathOr(500, ['response', 'status'], error);
92
+ result = R.pathOr({ message: error.message }, ['response', 'data'], error);
93
+ } finally {
94
+ if (server) {
95
+ await new Promise(resolve => server.close(resolve));
96
+ jobLog(`Worker internal server for job ${jobId} closed.`);
97
+ }
98
+ }
99
+ resultValue = { code, result, success: code >= 200 && code < 300 };
100
+
101
+ } else if (type === 'callback') {
102
+ const { callbackUrl, request, result } = jobDataPayload; // jobDataPayload is job.data.payload
103
+ jobLog(`sending callback to ${callbackUrl}`);
104
+ await require('../lib/callback').sendCallback({
105
+ callbackUrl,
106
+ request,
107
+ result,
108
+ });
109
+ resultValue = { success: true };
110
+
111
+ } else if (type === 'plugin') {
112
+ // jobDataPayload is job.data.payload
113
+ // jobDataMethod is job.data.method (for generic plugin calls)
114
+
115
+ // Check for our specific controller-level background task
116
+ if (jobDataPayload && jobDataPayload.methodName === '$bookingsProductSearchInternal') {
117
+ jobLog(`Processing via bookingsCtrl.$bookingsProductSearchInternal`);
118
+
119
+ const controllerArgs = jobDataPayload.args;
120
+ if (!controllerArgs) {
121
+ throw new Error('$bookingsProductSearchInternal job is missing "args" in its payload');
122
+ }
123
+
124
+ // Ensure appKey from args matches pluginName from job data if pluginName is used for context
125
+ if (pluginName && controllerArgs.appKey !== pluginName) {
126
+ jobErr(`Job data pluginName (${pluginName}) mismatch with args.appKey (${controllerArgs.appKey}). Using args.appKey for plugin context.`);
127
+ }
128
+ // If this job type implies a forced refresh, `forceRefresh: true` should be part of `controllerArgs`.
129
+ // The `backgroundJob` flag is removed from $bookingsProductSearch.
130
+ const executionPayload = {
131
+ ...controllerArgs, // Contains appKey, userId, hint, payload (for $bps), headers
132
+ // Potentially `forceRefresh: true` if needed for this job type.
133
+ axios: mainAxios, // Provide the worker's generic axios instance
134
+ requestId: jobId, // Use job ID for tracing
135
+ // plugins are already available to bookingsCtrl via its factory initialization
136
+ };
137
+
138
+ resultValue = await bookingsCtrl.$bookingsProductSearch(executionPayload);
139
+ jobLog(`bookingsCtrl.$bookingsProductSearchInternal completed.`);
140
+
141
+ } else {
142
+ // Handle generic plugin method call
143
+ jobLog(`Processing plugin method: ${jobDataMethod} for plugin ${pluginName}`);
144
+
145
+ const currentJobPlugins = await (async () => {
146
+ if (inTesting) {
147
+ const fakePlugin = require('../test/plugin');
148
+ const FakePluginClass = fakePlugin.default || fakePlugin;
149
+ return [...pluginsParam, new FakePluginClass({ name: pluginName })];
150
+ }
151
+ return pluginsParam;
152
+ })();
153
+
154
+ const thePlugin = currentJobPlugins.find(({ name }) => name === pluginName);
155
+
156
+ if (!thePlugin) {
157
+ throw new Error(`Plugin ${pluginName} not found for job ${jobId}.`);
158
+ }
159
+ if (!jobDataMethod || typeof thePlugin[jobDataMethod] !== 'function') {
160
+ throw new Error(`Method ${jobDataMethod} not found or not a function on plugin ${pluginName} for job ${jobId}.`);
161
+ }
162
+
163
+ // Execute the plugin method
164
+ const pluginMethodResult = await thePlugin[jobDataMethod]({
165
+ ...(jobDataPayload || {}), // Spread job.data.payload (e.g., contains inner 'payload' and 'userId')
166
+ token: data.token, // Pass job.data.token
167
+ plugins: currentJobPlugins,
168
+ typeDefsAndQueries: bookingsControllerFactory.typeDefsAndQueries, // As per existing pattern
169
+ axios: mainAxios, // Inject worker's axios instance
170
+ requestId: jobId, // Inject job ID as requestId
171
+ });
172
+
173
+ jobLog(`Plugin method ${jobDataMethod} for ${pluginName} completed.`);
174
+ resultValue = pluginMethodResult; // The primary result of the job is the plugin's output
175
+
176
+ // Post-processing step
177
+ if (data.postProcess) {
178
+ jobLog(`Initiating post-processing step: ${data.postProcess.controller}.${data.postProcess.action}`);
179
+ if (data.postProcess.controller === 'bookings' && bookingsCtrl[data.postProcess.action]) {
180
+ await bookingsCtrl[data.postProcess.action]({
181
+ ...(data.postProcess.args || {}), // Spread static args from job data
182
+ pluginResult: pluginMethodResult, // Pass the dynamic result from the plugin
183
+ requestId: jobId, // Pass job ID for tracing in post-process
184
+ // 'plugins' are already part of bookingsCtrl via its factory
185
+ });
186
+ jobLog(`Post-processing step ${data.postProcess.action} completed for job ${jobId}.`);
187
+ } else {
188
+ jobErr(`Post-processing action ${data.postProcess.action} on controller ${data.postProcess.controller} not found or controller not supported for job ${jobId}.`);
189
+ // Depending on requirements, this could throw an error to fail the job
190
+ }
191
+ }
192
+ }
193
+ } else {
194
+ jobErr(`Unknown job type: ${type} for job ${jobId}`);
195
+ throw new Error(`Unknown job type: ${type} for job ${jobId}`);
83
196
  }
84
- }
85
197
 
86
- return {
87
- code,
88
- result,
89
- success: code >= 200 && code < 300,
90
- };
91
- }
92
- if (type === 'callback') {
93
- const {
94
- callbackUrl,
95
- request,
96
- result,
97
- } = payload;
98
-
99
- console.log(`job ${jobId} > sending callback to ${callbackUrl}`);
100
- await require('../lib/callback').sendCallback({
101
- callbackUrl,
102
- request,
103
- result,
104
- });
105
- return { success: true };
106
- }
107
- console.log(`job ${jobId} > running ${pluginName}:${method} for ${userId}`);
108
- const plugins = await (async () => {
109
- if (inTesting) {
110
- const fakePlugin = require('../test/plugin');
111
- return [...pluginsParam, await new fakePlugin({ name: pluginName })];
198
+ jobLog(`completed`);
199
+ await saveResult({ id: jobId, resultValue });
200
+
201
+ } catch (error) {
202
+ jobErr(`Processing failed: ${error.message}`);
203
+ if (error.stack) jobErr(error.stack); // Log full stack for debugging
204
+ throw new Error(error.message) // Re-throw to mark job as failed in Bull (use a summarized mesage)
112
205
  }
113
- return pluginsParam;
114
- })();
115
- const thePlugin = plugins.find(({ name }) => name === pluginName);
116
- const resultValue = await thePlugin[method]({
117
- ...params.payload,
118
- token: params.token,
119
- plugins,
120
- typeDefsAndQueries: require('../controllers/bookings').typeDefsAndQueries,
121
206
  });
122
- console.log(`job ${jobId} > completed`);
123
- await saveResult({ id: jobId, resultValue });
124
- });
207
+ };
125
208
  };
126
209
 
127
210
  module.exports = async args => throng({
package/worker/queue.js CHANGED
@@ -1,7 +1,8 @@
1
1
  const Queue = require('bull');
2
- const Redis = require('ioredis');
3
2
  const R = require('ramda');
4
-
3
+ const Redis = require('ioredis');
4
+ const sqldb = require('../models');
5
+ const { env: { redisHost, redisPort } } = process;
5
6
  const REDIS_URL = process.env.REDIS_URL || 'redis://redis:6379';
6
7
  const itemsTTL = 3 * 60 * 60; // 3 hours
7
8
 
@@ -32,13 +33,15 @@ queue.on('failed', (job, err) => {
32
33
  const addJob = async (payload, paramsParam) => {
33
34
  const params = paramsParam || {};
34
35
  const inTesting = Boolean(process.env.JEST_WORKER_ID);
35
- const id = R.path(['id'], await queue.add({
36
+ const job = await queue.add({
36
37
  ...payload,
37
38
  inTesting,
38
39
  }, {
39
40
  removeOnComplete: true,
40
41
  ...params,
41
- }));
42
+ });
43
+ // For repeat jobs, we need to store the full repeat key
44
+ const id = job.opts.repeat ? job.opts.jobId : job.id;
42
45
  return id;
43
46
  };
44
47
 
@@ -46,6 +49,67 @@ const saveResult = async ({ id, resultValue }) => {
46
49
  await redisResults.set(id, JSON.stringify(resultValue), 'EX', itemsTTL);
47
50
  };
48
51
 
52
+ const removeJob = async (jobId) => {
53
+ // For repeat jobs, the ID is in the format 'repeat:jobId:timestamp'
54
+ const jobIdParts = jobId.split(':');
55
+ const isRepeatJob = jobIdParts[0] === 'repeat';
56
+
57
+ if (isRepeatJob) {
58
+ // Get all repeatable jobs
59
+ const repeatableJobs = await queue.getRepeatableJobs();
60
+
61
+ // Find the job with matching cron pattern
62
+ const cronJob = await sqldb.CronJobs.findOne({
63
+ where: {
64
+ bullJobId: jobId,
65
+ },
66
+ });
67
+
68
+ if (cronJob) {
69
+ // First remove all repeatable jobs with matching cron pattern
70
+ for (const repeatableJob of repeatableJobs) {
71
+ if (repeatableJob.cron === cronJob.cron) {
72
+ await queue.removeRepeatableByKey(repeatableJob.key);
73
+ }
74
+ }
75
+
76
+ // Then remove all jobs with this pattern
77
+ const jobs = await queue.getJobs(['active', 'wait', 'delayed']);
78
+ for (const job of jobs) {
79
+ if (job.opts.repeat && job.opts.jobId === jobId) {
80
+ await job.remove();
81
+ }
82
+ }
83
+
84
+ // Wait for the queue to process the removals
85
+ await new Promise(resolve => setTimeout(resolve, 500));
86
+
87
+ // Clean up any remaining jobs
88
+ const remainingJobs = await queue.getJobs(['active', 'wait', 'delayed']);
89
+ for (const job of remainingJobs) {
90
+ if (job.opts.repeat && job.opts.jobId === jobId) {
91
+ await job.remove();
92
+ }
93
+ }
94
+
95
+ await redisResults.del(jobId);
96
+ return;
97
+ }
98
+
99
+ // If we couldn't find the repeatable job, just delete from the database
100
+ // This can happen if the job was already removed from Bull but still exists in our database
101
+ return;
102
+ }
103
+
104
+ // If not a repeatable job, try to remove as a regular job
105
+ const job = await queue.getJob(jobId);
106
+ if (!job) {
107
+ throw new Error('Job not found');
108
+ }
109
+ await job.remove();
110
+ await redisResults.del(jobId);
111
+ };
112
+
49
113
  const jobStatus = async ({ jobId }) => {
50
114
  try {
51
115
  const job = await queue.getJob(jobId);
@@ -71,13 +135,13 @@ const jobStatus = async ({ jobId }) => {
71
135
  status: state.toLowerCase(),
72
136
  };
73
137
  } catch (err) {
74
- console.log(`could not get job status for ${jobId}`, err);
75
138
  return { // 500
76
139
  jobId,
77
140
  status: 'failed',
78
141
  };
79
142
  }
80
143
  };
144
+
81
145
  module.exports = {
82
146
  addJob,
83
147
  allDone,
@@ -85,4 +149,5 @@ module.exports = {
85
149
  queue,
86
150
  saveResult,
87
151
  redisResults,
152
+ removeJob,
88
153
  };