Skip to content

Commit 2c3de95

Browse files
committed
fix share workers
1 parent 5f6967e commit 2c3de95

File tree

4 files changed

+109
-19
lines changed

4 files changed

+109
-19
lines changed

docs/parallel.md

Lines changed: 56 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -364,37 +364,78 @@ workers.on(event.all.result, (status, completedTests, workerStats) => {
364364
365365
## Sharing Data Between Workers
366366
367-
NodeJS Workers can communicate between each other via messaging system. It may happen that you want to pass some data from one of the workers to other. For instance, you may want to share user credentials accross all tests. Data will be appended to a container.
367+
NodeJS Workers can communicate between each other via messaging system. CodeceptJS allows you to share data between different worker processes using the `share()` and `inject()` functions.
368368
369-
However, you can't access uninitialized data from a container, so to start, you need to initialize data first. Inside `bootstrap` function of the config we execute the `share` to initialize value:
369+
### Basic Usage
370370
371+
You can share data directly using the `share()` function and access it using `inject()`:
372+
373+
```js
374+
// In one test or worker
375+
share({ userData: { name: 'user', password: '123456' } });
376+
377+
// In another test or worker
378+
const testData = inject();
379+
console.log(testData.userData.name); // 'user'
380+
console.log(testData.userData.password); // '123456'
381+
```
382+
383+
### Initializing Data in Bootstrap
384+
385+
For complex scenarios where you need to initialize shared data before tests run, you can use the bootstrap function:
371386
372387
```js
373388
// inside codecept.conf.js
374389
exports.config = {
375390
bootstrap() {
376-
// append empty userData to container
377-
share({ userData: false });
391+
// Initialize shared data container
392+
share({ userData: null, config: { retries: 3 } });
378393
}
379394
}
380395
```
381396
382-
Now each worker has `userData` inside a container. However, it is empty.
383-
When you obtain real data in one of the tests you can now `share` this data accross tests. Use `inject` function to access data inside a container:
397+
Then in your tests, you can check and update the shared data:
384398
385399
```js
386-
// get current value of userData
387-
let { userData } = inject();
388-
// if userData is still empty - update it
389-
if (!userData) {
390-
userData = { name: 'user', password: '123456' };
391-
// now new userData will be shared accross all workers
392-
share({userData : userData});
400+
const testData = inject();
401+
if (!testData.userData) {
402+
// Update shared data - both approaches work:
403+
share({ userData: { name: 'user', password: '123456' } });
404+
// or mutate the injected object:
405+
testData.userData = { name: 'user', password: '123456' };
393406
}
394407
```
395408
396-
If you want to share data only within same worker, and not across all workers, you need to add option `local: true` every time you run `share`
409+
### Working with Proxy Objects
410+
411+
Since CodeceptJS 3.7.0+, shared data uses Proxy objects for synchronization between workers. The proxy system works seamlessly for most use cases:
412+
413+
```js
414+
// ✅ All of these work correctly:
415+
const data = inject();
416+
console.log(data.userData.name); // Access nested properties
417+
console.log(Object.keys(data)); // Enumerate shared keys
418+
data.newProperty = 'value'; // Add new properties
419+
Object.assign(data, { more: 'data' }); // Merge objects
420+
```
421+
422+
**Important Note:** Avoid reassigning the entire injected object:
423+
424+
```js
425+
// ❌ AVOID: This breaks the proxy reference
426+
let testData = inject();
427+
testData = someOtherObject; // This will NOT work as expected!
428+
429+
// ✅ PREFERRED: Use share() to replace data or mutate properties
430+
share({ userData: someOtherObject }); // This works!
431+
// or
432+
Object.assign(inject(), someOtherObject); // This works!
433+
```
434+
435+
### Local Data (Worker-Specific)
436+
437+
If you want to share data only within the same worker (not across all workers), use the `local` option:
397438
398439
```js
399-
share({ userData: false }, {local: true });
440+
share({ localData: 'worker-specific' }, { local: true });
400441
```

lib/container.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ let container = {
2828
translation: {},
2929
/** @type {Result | null} */
3030
result: null,
31+
sharedKeys: new Set() // Track keys shared via share() function
3132
}
3233

3334
/**
@@ -174,6 +175,7 @@ class Container {
174175
container.translation = loadTranslation()
175176
container.proxySupport = createSupportObjects(newSupport)
176177
container.plugins = newPlugins
178+
container.sharedKeys = new Set() // Clear shared keys
177179
asyncHelperPromise = Promise.resolve()
178180
store.actor = null
179181
debug('container cleared')
@@ -197,7 +199,13 @@ class Container {
197199
* @param {Object} options - set {local: true} to not share among workers
198200
*/
199201
static share(data, options = {}) {
200-
Container.append({ support: data })
202+
// Instead of using append which replaces the entire container,
203+
// directly update the support object to maintain proxy references
204+
Object.assign(container.support, data)
205+
206+
// Track which keys were explicitly shared
207+
Object.keys(data).forEach(key => container.sharedKeys.add(key))
208+
201209
if (!options.local) {
202210
WorkerStorage.share(data)
203211
}
@@ -396,10 +404,11 @@ function createSupportObjects(config) {
396404
{},
397405
{
398406
has(target, key) {
399-
return keys.includes(key)
407+
return keys.includes(key) || container.sharedKeys.has(key)
400408
},
401409
ownKeys() {
402-
return keys
410+
// Return both original config keys and explicitly shared keys
411+
return [...new Set([...keys, ...container.sharedKeys])]
403412
},
404413
getOwnPropertyDescriptor(target, prop) {
405414
return {
@@ -409,6 +418,10 @@ function createSupportObjects(config) {
409418
}
410419
},
411420
get(target, key) {
421+
// First check if this is an explicitly shared property
422+
if (container.sharedKeys.has(key) && key in container.support) {
423+
return container.support[key]
424+
}
412425
return lazyLoad(key)
413426
},
414427
},

lib/workerStorage.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ const invokeWorkerListeners = (workerObj) => {
77
const { threadId } = workerObj;
88
workerObj.on('message', (messageData) => {
99
if (messageData.event === shareEvent) {
10-
share(messageData.data);
10+
const Container = require('./container');
11+
Container.share(messageData.data);
1112
}
1213
});
1314
workerObj.on('exit', () => {

test/unit/workerStorage_test.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
const { expect } = require('expect');
2+
const WorkerStorage = require('../../lib/workerStorage');
3+
const { Worker } = require('worker_threads');
4+
const event = require('../../lib/event');
5+
6+
describe('WorkerStorage', () => {
7+
it('should handle share message correctly without circular dependency', (done) => {
8+
// Create a mock worker to test the functionality
9+
const mockWorker = {
10+
threadId: 'test-thread-1',
11+
on: (eventName, callback) => {
12+
if (eventName === 'message') {
13+
// Simulate receiving a share message
14+
setTimeout(() => {
15+
callback({ event: 'share', data: { testKey: 'testValue' } });
16+
done();
17+
}, 10);
18+
}
19+
},
20+
postMessage: () => {}
21+
};
22+
23+
// Add the mock worker to storage
24+
WorkerStorage.addWorker(mockWorker);
25+
});
26+
27+
it('should not crash when sharing data', () => {
28+
const testData = { user: 'test', password: '123' };
29+
30+
// This should not throw an error
31+
expect(() => {
32+
WorkerStorage.share(testData);
33+
}).not.toThrow();
34+
});
35+
});

0 commit comments

Comments
 (0)