Skip to content

Commit 08c61d4

Browse files
authored
Fix guard loading rules race conditions (#190)
1 parent 7d03e2f commit 08c61d4

File tree

2 files changed

+64
-19
lines changed

2 files changed

+64
-19
lines changed

src/services/guard/GuardService.ts

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ export class GuardService implements SettingsConfigurable, Closeable {
5555
// Track custom messages from rules files
5656
private readonly ruleCustomMessages = new Map<string, string>();
5757

58+
// Cache loaded rules
59+
private enabledRules: GuardRule[] = [];
60+
5861
// Validation queuing for concurrent requests
5962
private readonly validationQueue: ValidationQueueEntry[] = [];
6063
private readonly activeValidations = new Map<string, Promise<GuardViolation[]>>();
@@ -77,6 +80,15 @@ export class GuardService implements SettingsConfigurable, Closeable {
7780

7881
// Initialize rule configuration with current settings
7982
this.ruleConfiguration.updateFromSettings(this.settings);
83+
84+
// Load initial rules
85+
this.getEnabledRulesByConfiguration()
86+
.then((rules) => {
87+
this.enabledRules = rules;
88+
})
89+
.catch((error) => {
90+
this.log.error(`Failed to load initial rules: ${extractErrorMessage(error)}`);
91+
});
8092
}
8193

8294
/**
@@ -91,6 +103,15 @@ export class GuardService implements SettingsConfigurable, Closeable {
91103

92104
this.settings = settingsManager.getCurrentSettings().diagnostics.cfnGuard;
93105

106+
// Load rules with current settings
107+
this.getEnabledRulesByConfiguration()
108+
.then((rules) => {
109+
this.enabledRules = rules;
110+
})
111+
.catch((error) => {
112+
this.log.error(`Failed to load rules during configuration: ${extractErrorMessage(error)}`);
113+
});
114+
94115
// Subscribe to diagnostics settings changes
95116
this.settingsSubscription = settingsManager.subscribe('diagnostics', (newDiagnosticsSettings) => {
96117
this.onSettingsChanged(newDiagnosticsSettings.cfnGuard);
@@ -112,6 +133,17 @@ export class GuardService implements SettingsConfigurable, Closeable {
112133
const rulesFileChanged = previousSettings.rulesFile !== newSettings.rulesFile;
113134

114135
if (packListChanged || rulesFileChanged) {
136+
// Clear maps only when rule configuration actually changes
137+
this.ruleToPacksMap.clear();
138+
this.ruleCustomMessages.clear();
139+
// Preload rules with new settings
140+
this.getEnabledRulesByConfiguration()
141+
.then((rules) => {
142+
this.enabledRules = rules;
143+
})
144+
.catch((error) => {
145+
this.log.error(`Failed to preload rules after settings change: ${extractErrorMessage(error)}`);
146+
});
115147
this.revalidateAllDocuments();
116148
}
117149
}
@@ -182,15 +214,13 @@ export class GuardService implements SettingsConfigurable, Closeable {
182214
// Continue with validation but log the issues
183215
}
184216

185-
const enabledRules = await this.getEnabledRulesByConfiguration();
186-
187-
if (enabledRules.length === 0) {
217+
if (this.enabledRules.length === 0) {
188218
this.publishDiagnostics(uri, []);
189219
return;
190220
}
191221

192222
// Execute Guard validation with queuing for concurrent requests
193-
const violations = await this.queueValidation(uri, content, enabledRules);
223+
const violations = await this.queueValidation(uri, content, this.enabledRules);
194224

195225
// Convert violations to LSP diagnostics
196226
const diagnostics = this.convertViolationsToDiagnostics(uri, violations);
@@ -443,7 +473,7 @@ export class GuardService implements SettingsConfigurable, Closeable {
443473
/**
444474
* Process the validation queue
445475
*/
446-
private async processValidationQueue(): Promise<void> {
476+
private processValidationQueue(): void {
447477
if (this.isProcessingQueue || this.validationQueue.length === 0) {
448478
return;
449479
}
@@ -468,10 +498,10 @@ export class GuardService implements SettingsConfigurable, Closeable {
468498
continue;
469499
}
470500

471-
const enabledRules = await this.getEnabledRulesByConfiguration();
472-
473501
// Execute the validation
474-
this.executeValidation(entry.uri, entry.content, enabledRules).then(entry.resolve).catch(entry.reject);
502+
this.executeValidation(entry.uri, entry.content, this.enabledRules)
503+
.then(entry.resolve)
504+
.catch(entry.reject);
475505
}
476506
} finally {
477507
this.isProcessingQueue = false;
@@ -521,12 +551,6 @@ export class GuardService implements SettingsConfigurable, Closeable {
521551
private async getEnabledRulesByConfiguration(): Promise<GuardRule[]> {
522552
const enabledRules: GuardRule[] = [];
523553

524-
// Track which packs each rule comes from for proper reporting
525-
this.ruleToPacksMap.clear();
526-
527-
// Clear custom messages when getting new rules
528-
this.ruleCustomMessages.clear();
529-
530554
// If rulesFile is specified, load rules from file
531555
if (this.settings.rulesFile) {
532556
try {

tst/unit/services/guard/GuardService.test.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -452,16 +452,37 @@ describe('GuardService', () => {
452452
});
453453

454454
it('should show error diagnostic when rulesFile cannot be read', async () => {
455-
// When rulesFile is set but file doesn't exist, validation should catch the error
456-
// and publish error diagnostics to inform the user
455+
// Create a fresh settings manager for this test
456+
const mockSettingsManager = createMockSettingsManager({
457+
diagnostics: {
458+
cfnGuard: defaultSettings,
459+
},
460+
} as any);
461+
462+
guardService.configure(mockSettingsManager);
463+
464+
// Configure with invalid rules file to trigger async loading error
465+
const settingsCallback = mockSettingsManager.subscribe.getCall(0).args[1];
466+
467+
// Call the callback with settings that have an invalid rulesFile
468+
settingsCallback({
469+
cfnGuard: {
470+
...defaultSettings,
471+
rulesFile: '/nonexistent/rules.guard',
472+
},
473+
} as any);
474+
475+
// Wait a bit for async rule loading to complete
476+
await new Promise((resolve) => setTimeout(resolve, 10));
477+
478+
// Now validate - should use empty rules due to loading failure
457479
await guardService.validate('content', 'file:///test.yaml');
458480

459-
// Verify that publishDiagnostics was called with error diagnostic
481+
// Should publish empty diagnostics since rules failed to load
460482
expect(mockComponents.diagnosticCoordinator.publishDiagnostics.called).toBe(true);
461483
const call = mockComponents.diagnosticCoordinator.publishDiagnostics.getCall(0);
462484
const diagnostics = call.args[2];
463-
expect(diagnostics.length).toBeGreaterThan(0);
464-
expect(diagnostics[0].message).toContain('Guard Validation Error');
485+
expect(diagnostics.length).toBe(0); // Empty due to failed rule loading
465486
});
466487

467488
it('should parse multiple rules from rules file content', () => {

0 commit comments

Comments
 (0)