Skip to content

Commit d0d95ba

Browse files
authored
Merge pull request #200 from badaitech/fix/context-keypair-race-condition
Fix race condition by saving promise
2 parents 920a419 + 0b82b3e commit d0d95ba

2 files changed

Lines changed: 63 additions & 12 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright (c) 2025 BadLabs
3+
*
4+
* Use of this software is governed by the Business Source License 1.1 included in the file LICENSE.txt.
5+
*
6+
* As of the Change Date specified in that file, in accordance with the Business Source License, use of this software will be governed by the Apache License, version 2.0.
7+
*/
8+
9+
import { subtle } from 'node:crypto'
10+
import { setTimeout as sleep } from 'node:timers/promises'
11+
import { describe, expect, it, vi } from 'vitest'
12+
import { ExecutionContext } from '../execution-context'
13+
14+
describe('execution context', () => {
15+
it('has the same ECDH keypair after multiple calls', async () => {
16+
const abortController = new AbortController()
17+
const ctx = new ExecutionContext('', abortController)
18+
19+
const expected = await ctx.getECDHKeyPair()
20+
const actual = await ctx.getECDHKeyPair()
21+
22+
expect(actual).toStrictEqual(expected)
23+
})
24+
25+
it('has the same ECDH keypair for concurrent calls', async () => {
26+
const originalGenerateKey = subtle.generateKey.bind(subtle)
27+
28+
vi.spyOn(subtle, 'generateKey').mockImplementation(async (...args) => {
29+
// Introduce a delay to simulate async operation and increase the chance of race condition
30+
await sleep(10)
31+
return originalGenerateKey(...args)
32+
})
33+
34+
try {
35+
const abortController = new AbortController()
36+
const ctx = new ExecutionContext('', abortController)
37+
38+
const results = await Promise.all([
39+
ctx.getECDHKeyPair(),
40+
ctx.getECDHKeyPair(),
41+
ctx.getECDHKeyPair(),
42+
ctx.getECDHKeyPair(),
43+
ctx.getECDHKeyPair(),
44+
])
45+
46+
const expected = await subtle.exportKey('raw', results[0].publicKey)
47+
for (let i = 1; i < results.length; i++) {
48+
const actual = await subtle.exportKey('raw', results[i].publicKey)
49+
expect(actual).toStrictEqual(expected)
50+
}
51+
} finally {
52+
vi.spyOn(subtle, 'generateKey').mockRestore()
53+
}
54+
})
55+
})

packages/chaingraph-types/src/execution/execution-context.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export class ExecutionContext {
3434
public readonly metadata: Record<string, unknown>
3535
public readonly abortController: AbortController
3636

37-
private ecdhKeyPair?: CryptoKeyPair | null = null
37+
private ecdhKeyPairPromise: Promise<CryptoKeyPair> | null = null
3838

3939
// debug log events queue
4040
private readonly eventsQueue: EventQueue<ExecutionEvent>
@@ -96,19 +96,15 @@ export class ExecutionContext {
9696
return this.abortController.signal
9797
}
9898

99-
private async generateECDHKeyPair() {
100-
this.ecdhKeyPair = await subtle.generateKey({
101-
name: 'ECDH',
102-
namedCurve: 'P-256',
103-
}, false, ['deriveKey'])
104-
}
105-
10699
async getECDHKeyPair(): Promise<CryptoKeyPair> {
107-
if (this.ecdhKeyPair === null || this.ecdhKeyPair === undefined) {
108-
await this.generateECDHKeyPair()
100+
if (!this.ecdhKeyPairPromise) {
101+
this.ecdhKeyPairPromise = subtle.generateKey({
102+
name: 'ECDH',
103+
namedCurve: 'P-256',
104+
}, false, ['deriveKey'])
109105
}
110106

111-
return this.ecdhKeyPair!
107+
return this.ecdhKeyPairPromise
112108
}
113109

114110
async sendEvent(event: ExecutionEvent): Promise<void> {
@@ -167,7 +163,7 @@ export class ExecutionContext {
167163
eventData,
168164
true, // Is child execution
169165
)
170-
ctx.ecdhKeyPair = this.ecdhKeyPair // Share key pair if needed
166+
ctx.ecdhKeyPairPromise = this.ecdhKeyPairPromise // Share key pair promise
171167
return ctx
172168
}
173169
}

0 commit comments

Comments
 (0)