-
Notifications
You must be signed in to change notification settings - Fork 8
SDK-6983: rescue SDK-log upload on signal termination (v8 port of #86) #87
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v8
Are you sure you want to change the base?
Changes from all commits
ea9db70
4fc5bfa
50a06fe
aec8d08
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -750,6 +750,12 @@ export default class BrowserstackLauncherService implements Services.ServiceInst | |
|
|
||
| await PerformanceTester.measureWrapper(PERFORMANCE_SDK_EVENTS.EVENTS.SDK_UPLOAD_LOGS, async () => { | ||
| const response = await uploadLogs(getBrowserStackUser(this._config), getBrowserStackKey(this._config), clientBuildUuid) | ||
| const delivered = !!response && !(response.status && response.status !== 'success') | ||
| if (delivered) { | ||
| // A delivered upload must not be repeated by the exit-time | ||
| // cleanup rescue. | ||
| this.browserStackConfig.logsUploaded = true | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LOW —
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in aec8d08 — also covered the inverse case: a truthy server rejection ( |
||
| } | ||
| BStackLogger.logToFile(`Response - ${format(response)}`, 'debug') | ||
| })() | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest' | ||
| import { shouldCallCleanup, setupExitHandlers } from '../src/exitHandler.js' | ||
| import BrowserStackConfig from '../src/config.js' | ||
| import * as bstackLogger from '../src/bstackLogger.js' | ||
| import * as FunnelInstrumentation from '../src/instrumentation/funnelInstrumentation.js' | ||
| import PerformanceTester from '../src/instrumentation/performance/performance-tester.js' | ||
| import { BROWSERSTACK_TESTHUB_UUID } from '../src/constants.js' | ||
|
|
||
| vi.mock('node:child_process', () => ({ spawn: vi.fn(() => ({ unref: vi.fn() })) })) | ||
| vi.mock('../src/cli/index.js', () => ({ | ||
| BrowserstackCLI: { getInstance: () => ({ isRunning: () => false, process: null }) } | ||
| })) | ||
|
|
||
| vi.spyOn(bstackLogger.BStackLogger, 'logToFile').mockImplementation(() => {}) | ||
| vi.spyOn(FunnelInstrumentation, 'saveFunnelData').mockReturnValue('funnel.json') | ||
| vi.spyOn(PerformanceTester, 'isEnabled').mockReturnValue(false) | ||
|
|
||
| function makeConfig(overrides: Record<string, unknown> = {}) { | ||
| return { | ||
| userName: 'user', | ||
| accessKey: 'key', | ||
| funnelDataSent: true, | ||
| logsUploaded: false, | ||
| sdkRunID: 'run-123', | ||
| testObservability: { buildStopped: false }, | ||
| ...overrides | ||
| } as any | ||
| } | ||
|
|
||
| describe('shouldCallCleanup', () => { | ||
| let originalEnv: NodeJS.ProcessEnv | ||
|
|
||
| beforeEach(() => { | ||
| originalEnv = process.env | ||
| process.env = {} | ||
| }) | ||
|
|
||
| afterEach(() => { | ||
| process.env = originalEnv | ||
| }) | ||
|
|
||
| it('pushes --uploadLogs with the testhub uuid when logs are not yet uploaded', () => { | ||
| process.env[BROWSERSTACK_TESTHUB_UUID] = 'testhub-uuid' | ||
| const args = shouldCallCleanup(makeConfig()) | ||
| expect(args).toContain('--uploadLogs') | ||
| expect(args[args.indexOf('--uploadLogs') + 1]).toBe('testhub-uuid') | ||
| }) | ||
|
|
||
| it('falls back to sdkRunID when the testhub uuid is absent', () => { | ||
| const args = shouldCallCleanup(makeConfig()) | ||
| expect(args[args.indexOf('--uploadLogs') + 1]).toBe('run-123') | ||
| }) | ||
|
|
||
| it('omits --uploadLogs when logs were already uploaded', () => { | ||
| const args = shouldCallCleanup(makeConfig({ logsUploaded: true })) | ||
| expect(args).not.toContain('--uploadLogs') | ||
| }) | ||
|
|
||
| it('omits --uploadLogs when credentials are missing', () => { | ||
| const args = shouldCallCleanup(makeConfig({ userName: undefined, accessKey: undefined })) | ||
| expect(args).not.toContain('--uploadLogs') | ||
| }) | ||
| }) | ||
|
|
||
| describe('setupExitHandlers forced exit', () => { | ||
| let exitSpy: ReturnType<typeof vi.spyOn> | ||
|
|
||
| beforeEach(() => { | ||
| vi.useFakeTimers() | ||
| exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as never) | ||
| vi.spyOn(BrowserStackConfig, 'getInstance').mockReturnValue({ setKillSignal: vi.fn() } as any) | ||
| }) | ||
|
|
||
| afterEach(() => { | ||
| process.removeAllListeners('SIGTERM') | ||
| exitSpy.mockRestore() | ||
| vi.useRealTimers() | ||
| vi.restoreAllMocks() | ||
| }) | ||
|
|
||
| it('forces the conventional 128+n exit once the grace window elapses', () => { | ||
| setupExitHandlers() | ||
| process.emit('SIGTERM' as NodeJS.Signals) | ||
|
|
||
| expect(exitSpy).not.toHaveBeenCalled() | ||
|
|
||
| vi.advanceTimersByTime(5000) | ||
|
|
||
| expect(exitSpy).toHaveBeenCalledWith(143) | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
HIGH — signal handlers never re-exit. Same as #86: the handler only sets the kill flag + env, never
process.exit()/re-raises. SIGTERM/SIGHUP/SIGQUIT/SIGABRT no longer terminate the process — it hangs until CI sends SIGKILL, which fires no'exit'event, so the cleanup child never spawns and the log-upload rescue this PR adds may not fire in its own target scenario. Drive a deterministic exit after stamping the signal, without preempting WDIO's graceful shutdown.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in aec8d08 — same treatment as #86: after stamping the kill flag/env, the listener arms an
unref()'d 5s grace timer that forcesprocess.exit(128 + n)(SIGTERM→143) if nothing else has terminated the process.process.exitfires the'exit'listener, so the detached cleanup child (and this PR's--uploadLogsrescue) runs even on the hung-shutdown path; a naturally exiting run is unaffected. Unit-tested with fake timers.