Skip to content

Add dynamix refresh service, debounce store sync, and use ConfigService for notifications path#1888

Open
elibosley wants to merge 2 commits intomainfrom
codex/troubleshoot-emfile-error-on-server-hvhhcu
Open

Add dynamix refresh service, debounce store sync, and use ConfigService for notifications path#1888
elibosley wants to merge 2 commits intomainfrom
codex/troubleshoot-emfile-error-on-server-hvhhcu

Conversation

@elibosley
Copy link
Member

@elibosley elibosley commented Mar 5, 2026

Motivation

  • Avoid repeatedly reloading the dynamix config from getters and reduce redundant dispatches when the file on disk hasn't changed.
  • Prevent rapid, repeated writes of the entire Redux state into Nest ConfigService by debouncing and avoiding identical writes.
  • Make notification directory configuration injectable and testable by using ConfigService rather than reading directly from store getters.
  • Ensure plugin error handling can create notifications without relying on global store getters.

Description

  • Changed getters.dynamix to return store.getState().dynamix directly instead of calling loadDynamixConfig from the getter.
  • Added DynamixConfigRefreshService to periodically reload dynamix config from disk (DYNAMIX_REFRESH_INTERVAL_MS = 5000), dispatch updates only when the serialized config changes, and dispatch FAILED_LOADING on errors, and registered the service in LegacyConfigModule.
  • Implemented debounced sync in StoreSyncService with STORE_SYNC_DEBOUNCE_MS = 1000, tracking lastSyncedState to skip redundant writes and clearing timers on destroy.
  • Updated NotificationsService to accept ConfigService via DI, read the notification path from configService.get('store.dynamix.notify.path', '/tmp/notifications'), and updated constructor/paths() accordingly to recreate the watcher when the configured path changes.
  • Updated PluginService to instantiate NotificationsService with a ConfigService when reporting plugin load errors so notifications can be created without relying on store getters.
  • Added unit tests for the new/updated behavior: dynamix-config-refresh.service.spec.ts, store-sync.service.spec.ts, and adjusted notifications tests (loadNotificationsFile.test.ts, notifications.service.spec.ts) to provide ConfigService in test modules or helpers.

Testing

  • Ran unit tests with Vitest for the new service specs dynamix-config-refresh.service.spec.ts and store-sync.service.spec.ts and the updated notifications specs, and all tests completed successfully.
  • Ran the NotificationsService integration-style specs that interact with the filesystem, and they passed locally under the updated ConfigService-based configuration.
  • Verified no regressions in related tests that were updated to inject ConfigService or to use the new helper createNotificationsService.

Codex Task

Summary by CodeRabbit

Release Notes

  • New Features

    • Dynamix configuration now automatically refreshes at regular 5-second intervals to maintain synchronization with disk changes
    • Intelligent caching prevents redundant state updates when configuration remains unchanged
  • Tests

    • Added comprehensive test coverage for configuration refresh behavior and error handling

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 5, 2026

Walkthrough

A new periodic dynamix config refresh service is introduced that automatically loads configuration from disk at 5-second intervals. The store getter is simplified to return persisted state directly, while the service handles actual config loading and synchronization logic on module initialization.

Changes

Cohort / File(s) Summary
DynamixConfigRefreshService
api/src/unraid-api/config/dynamix-config-refresh.service.ts, api/src/unraid-api/config/dynamix-config-refresh.service.spec.ts
New NestJS service that periodically refreshes dynamix config from disk every 5 seconds, detects changes via serialization comparison, dispatches store updates on change, and handles loading errors with appropriate status flags. Comprehensive test suite validates initialization, no-op behavior for unchanged configs, and error handling.
Store State Management
api/src/store/index.ts
Simplified dynamix getter to return persisted store state directly instead of invoking loadDynamixConfig(), removing side effects from getter access.
Module Configuration
api/src/unraid-api/config/legacy-config.module.ts
Registers DynamixConfigRefreshService in LegacyConfigModule providers and exports alongside StoreSyncService.

Sequence Diagram

sequenceDiagram
    participant Module as NestJS Module
    participant Service as DynamixConfigRefreshService
    participant FileSystem as File System
    participant Store as Store
    participant Logger as Logger

    Module->>Service: onModuleInit()
    Service->>Service: Start 5s interval timer
    
    loop Every 5 seconds
        Service->>Store: Get dynamix-config paths
        Store-->>Service: Config paths
        Service->>FileSystem: loadDynamixConfigFromDiskSync()
        alt Success
            FileSystem-->>Service: Config data
            Service->>Service: Serialize config
            alt Config changed
                Service->>Store: updateDynamixConfig(config, LOADED)
                Store->>Store: Update state
            else Config unchanged
                Service->>Service: Skip dispatch
            end
        else Loading fails
            FileSystem-->>Service: Error
            Logger->>Logger: Log error
            Service->>Store: updateDynamixConfig(_, FAILED_LOADING)
            Store->>Store: Update state
        end
    end
    
    Module->>Service: onModuleDestroy()
    Service->>Service: Clear interval timer
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 A service hops through file systems deep,
Refreshing configs while the app's asleep,
Five seconds tick, the changes bloom,
The store stays fresh in NestJS room! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes all three main changes: adding dynamix refresh service, implementing debounced store sync, and using ConfigService for notifications path.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/troubleshoot-emfile-error-on-server-hvhhcu

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 621f9064df

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +38 to +39
if (serializedConfig === this.lastSerializedConfig) {
return;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Re-dispatch loaded status after transient refresh failures

The unchanged-config guard returns before any status update, which means a transient read error can permanently leave dynamix.status as FAILED_LOADING: the catch block dispatches failure but keeps lastSerializedConfig, and when the same config content becomes readable again, this early return skips the LOADED dispatch. In that scenario, consumers that gate behavior on FileLoadStatus.LOADED will stay in an error state until the file contents actually change.

Useful? React with 👍 / 👎.

} catch (error) {
PluginService.logger.error(`Plugin from ${pkgName} is invalid: %o`, error as object);
const notificationService = new NotificationsService();
const notificationService = new NotificationsService(new ConfigService());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use active app config when creating plugin error notifications

Constructing NotificationsService with new ConfigService() creates an empty config source, so NotificationsService.getConfiguredPath() falls back to /tmp/notifications instead of the runtime store.dynamix.notify.path. If users configure a non-default notification path, plugin import errors will be written to the wrong directory and won't show up where the rest of notifications are read.

Useful? React with 👍 / 👎.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
api/src/unraid-api/config/dynamix-config-refresh.service.spec.ts (1)

53-88: Add regression tests for paths missing and fail→recover status transition.
Current cases don’t cover the Line 33 crash path (paths undefined) or the scenario where a failure is followed by a successful reload with unchanged config.

If useful, I can draft the two Vitest cases directly in this spec.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/src/unraid-api/config/dynamix-config-refresh.service.spec.ts` around
lines 53 - 88, Add two Vitest cases to cover the regression: (1) a test where
loadDynamixConfigFromDiskSync returns an object without a paths property (e.g. {
notify: {...} } or {} ) and ensure service.onModuleInit() does not throw and
still calls updateDynamixConfig/dispatch appropriately; (2) a test simulating a
failure first (mock loadDynamixConfigFromDiskSync to throw) then a subsequent
successful load with the same config (mock implementations and advance timers)
and assert updateDynamixConfig/dispatch are called for the FAILED_LOADING then
called again for the recovery but not duplicated when the recovered config is
unchanged; use the existing helpers/mocks (loadDynamixConfigFromDiskSync,
service.onModuleInit, updateDynamixConfig, dispatch, FileLoadStatus) to locate
where to add these specs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/src/unraid-api/config/dynamix-config-refresh.service.ts`:
- Around line 38-40: The dedupe check using lastSerializedConfig prevents
dispatching LOADED after a prior FAILED_LOADING when the succeeding config is
identical; fix by not updating lastSerializedConfig on failures and by clearing
or nulling lastSerializedConfig in error paths so identical successful payloads
re-trigger processing. Specifically, in the block comparing serializedConfig to
lastSerializedConfig and in the error/catch handling (references:
serializedConfig, lastSerializedConfig, and the FAILED_LOADING / LOADED
dispatches), ensure lastSerializedConfig is only assigned after successful
handling (after dispatching LOADED) and that any catch/failure path resets
lastSerializedConfig (or leaves it unset) so a later successful match will not
be deduped.
- Line 33: Guard access to store.getState().paths before indexing to avoid
crashes when paths is undefined: replace the direct indexing in the assignment
to configPaths with a safe access (e.g. use optional chaining or a default
object). Concretely, update the line that assigns configPaths (currently using
store.getState().paths['dynamix-config'] ?? []) to something like
store.getState().paths?.['dynamix-config'] ?? [] or first capture const paths =
store.getState().paths ?? {} and then read paths['dynamix-config'] ?? []; this
change in dynamix-config-refresh.service.ts (the assignment creating
configPaths) will prevent startup errors when paths is undefined.

---

Nitpick comments:
In `@api/src/unraid-api/config/dynamix-config-refresh.service.spec.ts`:
- Around line 53-88: Add two Vitest cases to cover the regression: (1) a test
where loadDynamixConfigFromDiskSync returns an object without a paths property
(e.g. { notify: {...} } or {} ) and ensure service.onModuleInit() does not throw
and still calls updateDynamixConfig/dispatch appropriately; (2) a test
simulating a failure first (mock loadDynamixConfigFromDiskSync to throw) then a
subsequent successful load with the same config (mock implementations and
advance timers) and assert updateDynamixConfig/dispatch are called for the
FAILED_LOADING then called again for the recovery but not duplicated when the
recovered config is unchanged; use the existing helpers/mocks
(loadDynamixConfigFromDiskSync, service.onModuleInit, updateDynamixConfig,
dispatch, FileLoadStatus) to locate where to add these specs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a54b3a77-ee68-4b0e-978a-3b680783be3d

📥 Commits

Reviewing files that changed from the base of the PR and between 7956987 and fdf3bde.

📒 Files selected for processing (4)
  • api/src/store/index.ts
  • api/src/unraid-api/config/dynamix-config-refresh.service.spec.ts
  • api/src/unraid-api/config/dynamix-config-refresh.service.ts
  • api/src/unraid-api/config/legacy-config.module.ts

}

private refresh() {
const configPaths = store.getState().paths['dynamix-config'] ?? [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Guard paths before indexing to prevent startup crash.
Line 33 can throw when store.getState().paths is undefined (matches CI failure), which breaks module init.

💡 Proposed fix
-    private refresh() {
-        const configPaths = store.getState().paths['dynamix-config'] ?? [];
-
-        try {
+    private refresh() {
+        try {
+            const configPaths = store.getState().paths?.['dynamix-config'] ?? [];
             const config = loadDynamixConfigFromDiskSync(configPaths);
             const serializedConfig = JSON.stringify(config);
🧰 Tools
🪛 GitHub Actions: CI - Main (API)

[error] 33-33: Cannot read properties of undefined (reading 'dynamix-config') during DynamixConfigRefreshService.refresh invoked on AppModule initialization (AppModule Integration Tests).

🪛 GitHub Check: Test API

[failure] 33-33: src/unraid-api/app/test/app.module.integration.spec.ts > AppModule Integration Tests
TypeError: Cannot read properties of undefined (reading 'dynamix-config')
❯ DynamixConfigRefreshService.refresh src/unraid-api/config/dynamix-config-refresh.service.ts:33:51
❯ DynamixConfigRefreshService.onModuleInit src/unraid-api/config/dynamix-config-refresh.service.ts:17:14
❯ MapIterator.iteratee ../node_modules/.pnpm/@nestjs+core@11.1.6_@nestjs+common@11.1.6_class-transformer@0.5.1_class-validator@0.14.bc2bc0d32b3785c29eaf84bdeadd843f/node_modules/@nestjs/core/hooks/on-module-init.hook.js:22:43
❯ MapIterator.next ../node_modules/.pnpm/iterare@1.2.1/node_modules/iterare/src/map.ts:9:39
❯ IteratorWithOperators.next ../node_modules/.pnpm/iterare@1.2.1/node_modules/iterare/src/iterate.ts:19:28
❯ IteratorWithOperators.toArray ../node_modules/.pnpm/iterare@1.2.1/node_modules/iterare/src/iterate.ts:227:22
❯ callOperator ../node_modules/.pnpm/@nestjs+core@11.1.6
@nestjs+common@11.1.6_class-transformer@0.5.1_class-validator@0.14.bc2bc0d32b3785c29eaf84bdeadd843f/node_modules/@nestjs/core/hooks/on-module-init.hook.js:23:10
❯ callModuleInitHook ../node_modules/.pnpm/@nestjs+core@11.1.6
@nestjs+common@11.1.6_class-transformer@0.5.1_class-validator@0.14.bc2bc0d32b3785c29eaf84bdeadd843f/node_modules/@nestjs/core/hooks/on-module-init.hook.js:43:23
❯ Proxy.callInitHook ../node_modules/.pnpm/@nestjs+core@11.1.6
@nestjs+common@11.1.6_class-transformer@0.5.1_class-validator@0.14._bc2bc0d32b3785c29eaf84bdeadd843f/node_modules/@nestjs/core/nest-application-context.js:242:50

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/src/unraid-api/config/dynamix-config-refresh.service.ts` at line 33,
Guard access to store.getState().paths before indexing to avoid crashes when
paths is undefined: replace the direct indexing in the assignment to configPaths
with a safe access (e.g. use optional chaining or a default object). Concretely,
update the line that assigns configPaths (currently using
store.getState().paths['dynamix-config'] ?? []) to something like
store.getState().paths?.['dynamix-config'] ?? [] or first capture const paths =
store.getState().paths ?? {} and then read paths['dynamix-config'] ?? []; this
change in dynamix-config-refresh.service.ts (the assignment creating
configPaths) will prevent startup errors when paths is undefined.

Comment on lines +38 to +40
if (serializedConfig === this.lastSerializedConfig) {
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reset dedupe state after errors so successful recovery dispatches LOADED.
With current logic, a failure can leave state stuck as FAILED_LOADING if the next successful config matches the previous serialized value.

💡 Proposed fix
         } catch (error) {
             this.logger.error(error, 'Failed to refresh dynamix config from disk');
             store.dispatch(
                 updateDynamixConfig({
                     status: FileLoadStatus.FAILED_LOADING,
                 })
             );
+            this.lastSerializedConfig = null;
         }

Also applies to: 49-55

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/src/unraid-api/config/dynamix-config-refresh.service.ts` around lines 38
- 40, The dedupe check using lastSerializedConfig prevents dispatching LOADED
after a prior FAILED_LOADING when the succeeding config is identical; fix by not
updating lastSerializedConfig on failures and by clearing or nulling
lastSerializedConfig in error paths so identical successful payloads re-trigger
processing. Specifically, in the block comparing serializedConfig to
lastSerializedConfig and in the error/catch handling (references:
serializedConfig, lastSerializedConfig, and the FAILED_LOADING / LOADED
dispatches), ensure lastSerializedConfig is only assigned after successful
handling (after dispatching LOADED) and that any catch/failure path resets
lastSerializedConfig (or leaves it unset) so a later successful match will not
be deduped.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant