From 9c42e510821b7a5fc301aa76fcfd0a960dd3286c Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Tue, 25 Aug 2026 10:51:17 -0400 Subject: [PATCH] fix(connect): serialize nginx reload requests --- .../unraid-api/nginx/nginx.service.spec.ts | 36 +++++++++++++++++++ api/src/unraid-api/nginx/nginx.service.ts | 20 ++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 api/src/unraid-api/nginx/nginx.service.spec.ts diff --git a/api/src/unraid-api/nginx/nginx.service.spec.ts b/api/src/unraid-api/nginx/nginx.service.spec.ts new file mode 100644 index 0000000000..5f4d343d83 --- /dev/null +++ b/api/src/unraid-api/nginx/nginx.service.spec.ts @@ -0,0 +1,36 @@ +import { execa } from 'execa'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { NginxService } from '@app/unraid-api/nginx/nginx.service.js'; + +vi.mock('execa', () => ({ + execa: vi.fn(), +})); + +describe('NginxService', () => { + const mockExeca = vi.mocked(execa); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('coalesces concurrent reload requests', async () => { + const firstService = new NginxService(); + const secondService = new NginxService(); + + const results = await Promise.all([firstService.reload(), secondService.reload()]); + + expect(results).toEqual([true, true]); + expect(mockExeca).toHaveBeenCalledTimes(1); + expect(mockExeca).toHaveBeenCalledWith('/etc/rc.d/rc.nginx', ['reload']); + }); + + it('allows a new reload after the previous one completes', async () => { + const service = new NginxService(); + + await service.reload(); + await service.reload(); + + expect(mockExeca).toHaveBeenCalledTimes(2); + }); +}); diff --git a/api/src/unraid-api/nginx/nginx.service.ts b/api/src/unraid-api/nginx/nginx.service.ts index 675ccbe662..42461a5c63 100644 --- a/api/src/unraid-api/nginx/nginx.service.ts +++ b/api/src/unraid-api/nginx/nginx.service.ts @@ -2,12 +2,30 @@ import { Injectable, Logger } from '@nestjs/common'; import { execa } from 'execa'; +let activeReload: Promise | undefined; + @Injectable() export class NginxService { private readonly logger = new Logger(NginxService.name); /** reloads nginx via its rc script */ - async reload() { + async reload(): Promise { + if (activeReload) { + this.logger.debug('Nginx reload already in progress; waiting for it to complete'); + return activeReload; + } + + const reload = this.executeReload(); + activeReload = reload; + + try { + return await reload; + } finally { + activeReload = undefined; + } + } + + private async executeReload(): Promise { try { await execa('/etc/rc.d/rc.nginx', ['reload']); this.logger.log('Nginx reloaded');