-
Notifications
You must be signed in to change notification settings - Fork 18
fix: too many file descriptors with thousands of notifications #1887
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
Changes from all commits
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 |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| import { ConfigService } from '@nestjs/config'; | ||
|
|
||
| import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; | ||
|
|
||
| import { StoreSyncService } from '@app/unraid-api/config/store-sync.service.js'; | ||
|
|
||
| const { subscribe, getState } = vi.hoisted(() => ({ | ||
| subscribe: vi.fn(), | ||
| getState: vi.fn(), | ||
| })); | ||
|
|
||
| vi.mock('@app/store/index.js', () => ({ | ||
| store: { | ||
| subscribe, | ||
| getState, | ||
| }, | ||
| })); | ||
|
|
||
| describe('StoreSyncService', () => { | ||
| let service: StoreSyncService; | ||
| let configService: ConfigService; | ||
| let setSpy: ReturnType<typeof vi.spyOn>; | ||
| let unsubscribe: ReturnType<typeof vi.fn>; | ||
| let subscriber: (() => void) | undefined; | ||
|
|
||
| beforeEach(() => { | ||
| vi.useFakeTimers(); | ||
| subscribe.mockReset(); | ||
| getState.mockReset(); | ||
|
|
||
| unsubscribe = vi.fn(); | ||
| subscriber = undefined; | ||
|
|
||
| subscribe.mockImplementation((callback: () => void) => { | ||
| subscriber = callback; | ||
| return unsubscribe; | ||
| }); | ||
|
|
||
| configService = new ConfigService(); | ||
| setSpy = vi.spyOn(configService, 'set'); | ||
|
|
||
| service = new StoreSyncService(configService); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| vi.runOnlyPendingTimers(); | ||
| vi.useRealTimers(); | ||
| }); | ||
|
|
||
| it('debounces sync operations and writes once after rapid updates', () => { | ||
| getState.mockReturnValue({ count: 2 }); | ||
|
|
||
| subscriber?.(); | ||
| vi.advanceTimersByTime(500); | ||
| subscriber?.(); | ||
|
|
||
| expect(setSpy).not.toHaveBeenCalled(); | ||
|
|
||
| vi.advanceTimersByTime(1000); | ||
|
|
||
| expect(setSpy).toHaveBeenCalledTimes(1); | ||
| expect(setSpy).toHaveBeenCalledWith('store', { count: 2 }); | ||
| }); | ||
|
|
||
| it('skips writes when serialized state is unchanged', () => { | ||
| getState.mockReturnValue({ count: 1 }); | ||
|
|
||
| subscriber?.(); | ||
| vi.advanceTimersByTime(1000); | ||
|
|
||
| subscriber?.(); | ||
| vi.advanceTimersByTime(1000); | ||
|
|
||
| expect(setSpy).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| it('unsubscribes and clears timer on module destroy', () => { | ||
| getState.mockReturnValue({ count: 1 }); | ||
|
|
||
| subscriber?.(); | ||
| service.onModuleDestroy(); | ||
| vi.advanceTimersByTime(1000); | ||
|
|
||
| expect(unsubscribe).toHaveBeenCalledTimes(1); | ||
| expect(setSpy).not.toHaveBeenCalled(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| import { Injectable } from '@nestjs/common'; | ||
| import { ConfigService } from '@nestjs/config'; | ||
|
|
||
| import type { ApiNestPluginDefinition } from '@app/unraid-api/plugin/plugin.interface.js'; | ||
| import { pluginLogger } from '@app/core/log.js'; | ||
|
|
@@ -50,7 +51,7 @@ export class PluginService { | |
| }; | ||
| } catch (error) { | ||
| PluginService.logger.error(`Plugin from ${pkgName} is invalid: %o`, error as object); | ||
| const notificationService = new NotificationsService(); | ||
| const notificationService = new NotificationsService(new ConfigService()); | ||
|
Contributor
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. Potential issue: manually instantiated ConfigService lacks configuration context. Creating Since this is in a static method (
If the default path is acceptable for plugin error notifications, consider adding a comment explaining this intentional behavior. 💡 Optional: Add clarifying comment- const notificationService = new NotificationsService(new ConfigService());
+ // Note: Uses default config path since we're outside DI context during plugin load
+ const notificationService = new NotificationsService(new ConfigService());🤖 Prompt for AI Agents |
||
| const errorMessage = error?.toString?.() ?? (error as Error)?.message ?? ''; | ||
| await notificationService.createNotification({ | ||
| title: `Plugin from ${pkgName} is invalid`, | ||
|
|
||
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.
Making
ConfigServicea required constructor dependency breaks existing non-DI instantiation paths:PluginService.importPluginsstill callsnew NotificationsService()when plugin loading fails, sothis.configServiceisundefinedandgetConfiguredPath()throws beforecreateNotificationruns. In that failure path, the original plugin-validation error gets masked and the alert notification is never created, which regresses diagnostics for invalid plugins.Useful? React with 👍 / 👎.