Skip to content

Add system status handler#507

Open
chrisqm-dev wants to merge 9 commits intomainfrom
system-readiness
Open

Add system status handler#507
chrisqm-dev wants to merge 9 commits intomainfrom
system-readiness

Conversation

@chrisqm-dev
Copy link
Copy Markdown
Contributor

Description of changes:

  • Currently, the only way to tell if the schemas, lint, guard, and settings are successfully loaded is by checking the logs (for schemas, the logs only appear if the schemas needed to be downloaded) or by directly using an lsp request that uses the schemas
  • Added an LSP request to return numerous system components settings including:
    • regions where schemas are loaded
    • if lint is initialized
    • if guard is initialized
    • what settings are applied (since settings are propagated async)
    • This allows for more granular testing by decoupling the async initialization logic from the lsp request that is being tested

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@chrisqm-dev chrisqm-dev requested a review from a team as a code owner March 31, 2026 14:56

export type ReadinessStatus = {
ready: boolean;
reason?: string;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

do we need reason?

Copy link
Copy Markdown
Contributor Author

@chrisqm-dev chrisqm-dev Apr 1, 2026

Choose a reason for hiding this comment

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

Removed reason field

currentSettings: Settings;
};

export const GetSystemStatusRequestType = new RequestType<void, GetSystemStatusResponse, void>('aws/cfn/system/status');
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

aws/system/status

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed

return this.settingsState.toSettings();
}

/**
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is this comment required?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed

@Telemetry() private readonly telemetry!: ScopedTelemetry;
private readonly settingsState = new SettingsState();
private readonly subscriptionManager = new SubscriptionManager<Settings>();
private isUpdatingSettings = false;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think we need to know "has settings initialized or updated atleast once" - any workspace change or settings change will make this true, but that doesn't exactly say if the system is ready

Copy link
Copy Markdown
Contributor Author

@chrisqm-dev chrisqm-dev Apr 1, 2026

Choose a reason for hiding this comment

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

Modified isUpdatingSettings to instead be a settingsReady, it is initially set to false. If the syncConfiguration is completed successfully as part of the initializedHandler, the settings will be marked as ready. Upon making a settings update, settings readiness is set to false until the update successfully completes.

this.settingsState.update(newSettings);
logger.info(`Settings updated: ${toString(difference)}`);
this.subscriptionManager.notify(newSettings, oldSettings);
this.isUpdatingSettings = true;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Even if nothing has changed if settings have been synced atleast once, then settings are ready

Copy link
Copy Markdown
Contributor Author

@chrisqm-dev chrisqm-dev Apr 1, 2026

Choose a reason for hiding this comment

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

Modified isUpdatingSettings to instead be a settingsReady, it is initially set to false. If the syncConfiguration is completed successfully as part of the initializedHandler, the settings will be marked as ready. Upon making a settings update, settings readiness is set to false until the update successfully completes.

/**
* Get the readiness status of the Guard service
*/
public getReadinessStatus(): { ready: boolean; reason?: string } {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Use the types defined

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed

@satyakigh
Copy link
Copy Markdown
Collaborator

For settings and lint initializations, can the initialize handler be used?

@chrisqm-dev
Copy link
Copy Markdown
Contributor Author

chrisqm-dev commented Apr 1, 2026

For settings and lint initializations, can the initialize handler be used?

Yes, made updates so that:

  • inside initializeHandler we call syncConfiguration(), which will update the settings readiness status
  • Additionally, the InitializeHandler calls cfnLintService.initialize(), which will update the lint readiness status

@chrisqm-dev chrisqm-dev force-pushed the system-readiness branch 2 times, most recently from d77c62f to 39e76a4 Compare April 1, 2026 21:57
import { RequestType } from 'vscode-languageserver-protocol';
import { Settings } from '../settings/Settings';

export type ReadinessStatus = {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We're creating a new folder src/system just for these two types. Can we move this to src/protocol or inside src/protocol/LspSystemHandlers.ts. We already have tons of directories inside src. Would be good to avoid adding another one with 1 file, 2 types.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Deleted folder and moved types to LspSystemHandler.ts

export class LspSystemHandlers {
constructor(private readonly connection: Connection) {}

onGetSystemStatus(handler: ServerRequestHandler<void, GetSystemStatusResponse, never, void>) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why not just RequestHandler? We're not using progress reporter

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Changed method to use RequestHandler

const reason = settingsStatus.reason ?? 'Server initializing';
return {
settingsReady: settingsStatus,
schemasReady: { ready: false, reason, availableRegions: [] },
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Adding extra key to ReadinessStatus type just for schemas. Is this needed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed extra key, availableRegions is no longer supplied

schemasReady: {
availableRegions: [...availableRegions],
ready: availableRegions.length > 0,
...(availableRegions.length === 0 ? { reason: 'No schemas loaded' } : {}),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am not finding reason to be a helpful field. It might be simpler to keep the status response as just a ready or not flag.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed reason from status response

@Zee2413
Copy link
Copy Markdown
Contributor

Zee2413 commented Apr 3, 2026

Design Feedback: Interface-based Readiness

The current implementation has each component (CfnLintService, GuardService, SettingsManager) independently adding getReadinessStatus() with no shared contract, and schemas have no readiness method at all — the handler derives it inline. This creates three different patterns for the same concept:

  • CfnLintService: externally set via setReadinessStatus() from Initialize.ts
  • GuardService: internally derived from isLoadingRules + enabledRules.length
  • SettingsManager: internally managed via readinessStatus field
  • Schemas: no readiness method — handler checks getPublicSchemaRegions().length > 0 inline

The handler also contains aggregation logic (settings not ready → short-circuit all others as not ready), which is business logic that doesn't belong in a request handler.

Proposed: ReadinessContributor interface

Following the same pattern as Closeable and Configurable in this codebase, define a shared interface:

// src/utils/ReadinessContributor.ts

export type ReadinessStatus = {
    readonly ready: boolean;
};

export interface ReadinessContributor {
    isReady(): ReadinessStatus;
}

Each component implements it:

// CfnLintService
export class CfnLintService implements SettingsConfigurable, Closeable, ReadinessContributor {
    isReady(): ReadinessStatus {
        if (!this.settings.enabled) return { ready: true };
        return { ready: this.status === STATUS.Ready };
    }
}

// GuardService
export class GuardService implements SettingsConfigurable, Closeable, ReadinessContributor {
    isReady(): ReadinessStatus {
        if (!this.settings.enabled) return { ready: true };
        return { ready: !this.isLoadingRules && this.enabledRules.length > 0 };
    }
}

// SettingsManager
export class SettingsManager implements ISettingsSubscriber, ReadinessContributor {
    private initialized = false;

    isReady(): ReadinessStatus {
        return { ready: this.initialized };
    }
    // set this.initialized = true after first successful syncConfiguration()
}

For schemas, wrap SchemaStore since we shouldn't modify it for readiness concerns:

// src/schema/SchemaReadiness.ts
export class SchemaReadiness implements ReadinessContributor {
    constructor(private readonly schemaStore: SchemaStore) {}

    isReady(): ReadinessStatus {
        return { ready: this.schemaStore.getPublicSchemaRegions().length > 0 };
    }
}

Aggregator as single source of truth

// src/system/SystemReadiness.ts
export class SystemReadiness {
    constructor(
        private readonly settings: ReadinessContributor,
        private readonly schemas: ReadinessContributor,
        private readonly cfnLint: ReadinessContributor,
        private readonly cfnGuard: ReadinessContributor,
    ) {}

    getStatus(): GetSystemStatusResponse {
        return {
            settingsReady: this.settings.isReady(),
            schemasReady: this.schemas.isReady(),
            cfnLintReady: this.cfnLint.isReady(),
            cfnGuardReady: this.cfnGuard.isReady(),
        };
    }
}

Handler becomes trivial

export function getSystemStatusHandler(
    systemReadiness: SystemReadiness,
): RequestHandler<void, GetSystemStatusResponse, void> {
    return (): GetSystemStatusResponse => systemReadiness.getStatus();
}

Why this is better

  1. Uniform contract — no ad-hoc setReadinessStatus() on some components but not others
  2. Each component owns its own readiness — no external setting from Initialize.ts, no inline derivation in the handler
  3. Handler has zero logic — just delegates to the aggregator
  4. Extensible — adding a new component = implement interface + register in SystemReadiness
  5. Follows existing codebase patternsCloseable, Configurable are the same idea

Also agreeing with the other review comments:

  • src/system/ directory for 2 types is unnecessary
  • ServerRequestHandlerRequestHandler since progress reporting isn't used
  • availableRegions extending ReadinessStatus inline for schemas is messy — if region info is needed, it belongs on a separate endpoint or as a field on the response, not mixed into the readiness type


const handler = getSystemStatusHandler(mockComponents);

const result = handler(undefined, CancellationToken.None);

const handler = getSystemStatusHandler(mockComponents);

const result = handler(undefined, CancellationToken.None);
@chrisqm-dev
Copy link
Copy Markdown
Contributor Author

Design Feedback: Interface-based Readiness

The current implementation has each component (CfnLintService, GuardService, SettingsManager) independently adding getReadinessStatus() with no shared contract, and schemas have no readiness method at all — the handler derives it inline. This creates three different patterns for the same concept:

* **CfnLintService**: externally set via `setReadinessStatus()` from `Initialize.ts`

* **GuardService**: internally derived from `isLoadingRules` + `enabledRules.length`

* **SettingsManager**: internally managed via `readinessStatus` field

* **Schemas**: no readiness method — handler checks `getPublicSchemaRegions().length > 0` inline

The handler also contains aggregation logic (settings not ready → short-circuit all others as not ready), which is business logic that doesn't belong in a request handler.

Proposed: ReadinessContributor interface

Following the same pattern as Closeable and Configurable in this codebase, define a shared interface:

// src/utils/ReadinessContributor.ts

export type ReadinessStatus = {
    readonly ready: boolean;
};

export interface ReadinessContributor {
    isReady(): ReadinessStatus;
}

Each component implements it:

// CfnLintService
export class CfnLintService implements SettingsConfigurable, Closeable, ReadinessContributor {
    isReady(): ReadinessStatus {
        if (!this.settings.enabled) return { ready: true };
        return { ready: this.status === STATUS.Ready };
    }
}

// GuardService
export class GuardService implements SettingsConfigurable, Closeable, ReadinessContributor {
    isReady(): ReadinessStatus {
        if (!this.settings.enabled) return { ready: true };
        return { ready: !this.isLoadingRules && this.enabledRules.length > 0 };
    }
}

// SettingsManager
export class SettingsManager implements ISettingsSubscriber, ReadinessContributor {
    private initialized = false;

    isReady(): ReadinessStatus {
        return { ready: this.initialized };
    }
    // set this.initialized = true after first successful syncConfiguration()
}

For schemas, wrap SchemaStore since we shouldn't modify it for readiness concerns:

// src/schema/SchemaReadiness.ts
export class SchemaReadiness implements ReadinessContributor {
    constructor(private readonly schemaStore: SchemaStore) {}

    isReady(): ReadinessStatus {
        return { ready: this.schemaStore.getPublicSchemaRegions().length > 0 };
    }
}

Aggregator as single source of truth

// src/system/SystemReadiness.ts
export class SystemReadiness {
    constructor(
        private readonly settings: ReadinessContributor,
        private readonly schemas: ReadinessContributor,
        private readonly cfnLint: ReadinessContributor,
        private readonly cfnGuard: ReadinessContributor,
    ) {}

    getStatus(): GetSystemStatusResponse {
        return {
            settingsReady: this.settings.isReady(),
            schemasReady: this.schemas.isReady(),
            cfnLintReady: this.cfnLint.isReady(),
            cfnGuardReady: this.cfnGuard.isReady(),
        };
    }
}

Handler becomes trivial

export function getSystemStatusHandler(
    systemReadiness: SystemReadiness,
): RequestHandler<void, GetSystemStatusResponse, void> {
    return (): GetSystemStatusResponse => systemReadiness.getStatus();
}

Why this is better

1. **Uniform contract** — no ad-hoc `setReadinessStatus()` on some components but not others

2. **Each component owns its own readiness** — no external setting from `Initialize.ts`, no inline derivation in the handler

3. **Handler has zero logic** — just delegates to the aggregator

4. **Extensible** — adding a new component = implement interface + register in `SystemReadiness`

5. **Follows existing codebase patterns** — `Closeable`, `Configurable` are the same idea

Also agreeing with the other review comments:

* `src/system/` directory for 2 types is unnecessary

* `ServerRequestHandler` → `RequestHandler` since progress reporting isn't used

* `availableRegions` extending `ReadinessStatus` inline for schemas is messy — if region info is needed, it belongs on a separate endpoint or as a field on the response, not mixed into the readiness type

Added the ReadinessContributor interface and modified the services to implement it. I didn't add SystemReadiness because the handler was concise enough

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants