-
Notifications
You must be signed in to change notification settings - Fork 11k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: Omnichannel queue starting multiple times due to race condition (#…
…34062) Co-authored-by: Pierre Lehnen <[email protected]>
- Loading branch information
1 parent
18cea50
commit 072a749
Showing
6 changed files
with
206 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@rocket.chat/meteor": patch | ||
--- | ||
|
||
Fixes condition causing Omnichannel queue to start more than once. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
// This class is used to manage calls to a service's .start and .stop functions | ||
// Specifically for cases where the start function has different conditions that may cause the service to actually start or not, | ||
// or when the start process can take a while to complete | ||
// Using this class, you ensure that calls to .start and .stop will be chained, so you avoid race conditions | ||
// At the same time, it prevents those functions from running more times than necessary if there are several calls to them (for example when loading setting values) | ||
export class ServiceStarter { | ||
private lock = Promise.resolve(); | ||
|
||
private currentCall?: 'start' | 'stop'; | ||
|
||
private nextCall?: 'start' | 'stop'; | ||
|
||
private starterFn: () => Promise<void>; | ||
|
||
private stopperFn?: () => Promise<void>; | ||
|
||
constructor(starterFn: () => Promise<void>, stopperFn?: () => Promise<void>) { | ||
this.starterFn = starterFn; | ||
this.stopperFn = stopperFn; | ||
} | ||
|
||
private async checkStatus(): Promise<void> { | ||
if (this.nextCall === 'start') { | ||
return this.doCall('start'); | ||
} | ||
|
||
if (this.nextCall === 'stop') { | ||
return this.doCall('stop'); | ||
} | ||
} | ||
|
||
private async doCall(call: 'start' | 'stop'): Promise<void> { | ||
this.nextCall = undefined; | ||
this.currentCall = call; | ||
try { | ||
if (call === 'start') { | ||
await this.starterFn(); | ||
} else if (this.stopperFn) { | ||
await this.stopperFn(); | ||
} | ||
} finally { | ||
this.currentCall = undefined; | ||
await this.checkStatus(); | ||
} | ||
} | ||
|
||
private async call(call: 'start' | 'stop'): Promise<void> { | ||
// If something is already chained to run after the current call, it's okay to replace it with the new call | ||
this.nextCall = call; | ||
if (this.currentCall) { | ||
return this.lock; | ||
} | ||
this.lock = this.checkStatus(); | ||
return this.lock; | ||
} | ||
|
||
async start(): Promise<void> { | ||
return this.call('start'); | ||
} | ||
|
||
async stop(): Promise<void> { | ||
return this.call('stop'); | ||
} | ||
|
||
async wait(): Promise<void> { | ||
return this.lock; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import { ServiceStarter } from '../src/lib/ServiceStarter'; | ||
|
||
const wait = (time: number) => { | ||
return new Promise((resolve) => { | ||
setTimeout(() => resolve(undefined), time); | ||
}); | ||
}; | ||
|
||
describe('ServiceStarter', () => { | ||
it('should call the starterFn and stopperFn when calling .start and .stop', async () => { | ||
const start = jest.fn(); | ||
const stop = jest.fn(); | ||
|
||
const instance = new ServiceStarter(start, stop); | ||
|
||
expect(start).not.toHaveBeenCalled(); | ||
expect(stop).not.toHaveBeenCalled(); | ||
|
||
await instance.start(); | ||
|
||
expect(start).toHaveBeenCalled(); | ||
expect(stop).not.toHaveBeenCalled(); | ||
|
||
start.mockReset(); | ||
|
||
await instance.stop(); | ||
|
||
expect(start).not.toHaveBeenCalled(); | ||
expect(stop).toHaveBeenCalled(); | ||
}); | ||
|
||
it('should only call .start for the second time after the initial call has finished running', async () => { | ||
let running = false; | ||
const start = jest.fn(async () => { | ||
expect(running).toBe(false); | ||
|
||
running = true; | ||
await wait(100); | ||
running = false; | ||
}); | ||
const stop = jest.fn(); | ||
|
||
const instance = new ServiceStarter(start, stop); | ||
|
||
void instance.start(); | ||
void instance.start(); | ||
|
||
await instance.wait(); | ||
|
||
expect(start).toHaveBeenCalledTimes(2); | ||
expect(stop).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('should chain up to two calls to .start', async () => { | ||
const start = jest.fn(async () => { | ||
await wait(100); | ||
}); | ||
const stop = jest.fn(); | ||
|
||
const instance = new ServiceStarter(start, stop); | ||
|
||
void instance.start(); | ||
void instance.start(); | ||
void instance.start(); | ||
void instance.start(); | ||
|
||
await instance.wait(); | ||
|
||
expect(start).toHaveBeenCalledTimes(2); | ||
expect(stop).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('should skip the chained calls to .start if .stop is called', async () => { | ||
const start = jest.fn(async () => { | ||
await wait(100); | ||
}); | ||
const stop = jest.fn(); | ||
|
||
const instance = new ServiceStarter(start, stop); | ||
|
||
void instance.start(); | ||
void instance.start(); | ||
void instance.start(); | ||
void instance.stop(); | ||
|
||
await instance.wait(); | ||
|
||
expect(start).toHaveBeenCalledTimes(1); | ||
expect(stop).toHaveBeenCalledTimes(1); | ||
}); | ||
}); |