Files
slp-modular-cms/aidlc-docs/features/master-cms-module/inception/application-design/services.md
T

234 lines
9.5 KiB
Markdown

# Services — Master CMS Module
## Service Definitions
### Master-Side Services
#### CmsInstanceService
- **Interface**: `ICmsInstanceService`
- **Lifetime**: Scoped
- **Injected Dependencies**: `ICmsInstanceRepository`, `ISlaveApiClient`, `IOptions<MasterModuleOptions>`, `ILogger<CmsInstanceService>`
- **Responsibilities**: Business orchestration — coordinates repository (data) and SlaveApiClient (HTTP side-effects); enforces business rules (mandatory DisableMessage, Inactive cannot be pushed)
- **Key orchestration**: On `AddAsync` → persist entity then call `RegisterMasterAsync`; on `UpdateStatusAsync` → validate, persist, then call `PushStatusAsync`; on `VerifyIntegrityAsync` → query all active instances, call `GetRegisteredMasterUrlAsync` per instance, re-register on mismatch
#### SlaveApiClient
- **Interface**: `ISlaveApiClient`
- **Lifetime**: Transient (managed by `IHttpClientFactory`)
- **Registration**: `services.AddHttpClient<ISlaveApiClient, SlaveApiClient>()`
- **Injected Dependencies**: `HttpClient` (injected by framework)
- **Responsibilities**: Typed HTTP client for all Master → Slave API calls; sets `X-Master-Api-Key` header per call; deserializes responses; returns success flags rather than throwing (callers decide error handling)
- **Endpoints called**:
- `POST {slaveUrl}/api/internal/master/register`
- `PUT {slaveUrl}/api/v1/Availability/admin/status` (reuses existing slave endpoint)
- `GET {slaveUrl}/api/internal/master/registration` (integrity check)
#### IntegrityCheckBackgroundService
- **Lifetime**: Singleton (registered via `services.AddHostedService<IntegrityCheckBackgroundService>()`)
- **Injected Dependencies**: `IServiceScopeFactory`, `IOptions<MasterModuleOptions>`, `ILogger<IntegrityCheckBackgroundService>`
- **Responsibilities**: Periodic background loop; resolves `ICmsInstanceService` via `IServiceScopeFactory` per tick (required because `ICmsInstanceService` is Scoped); runs `VerifyIntegrityAsync()`; interval from `MasterModuleOptions.IntegrityCheckIntervalMinutes`
---
### Slave-Side Services
#### MasterAvailabilityService
- **Interface**: `IMasterAvailabilityService`
- **Lifetime**: Scoped
- **Injected Dependencies**: `AvailabilityDbContext`, `IHttpClientFactory`, `IOptions<MasterModuleOptions>`, `ILogger<MasterAvailabilityService>`
- **Responsibilities**: Checks DB for `MasterRegistration`; if none exists returns `HasMaster = false`; if exists checks cache freshness against `MasterModuleOptions.CacheMinutes`; pulls from Master via HTTP on cache miss; returns cached value on HTTP failure (fail-open, NFR-MASTER-01)
- **Cache fields** (static): `_cachedStatus` (default `Available`), `_cachedDisableMessage`, `_lastFetchedAt`, `_cachedMasterUrl`
- **Exemption**: No special logic needed — a Master CMS instance never calls `RegisterMaster` on itself, so `MasterRegistrations` table is empty → `HasMaster = false` always on a Master instance (FR-MASTER-09)
---
## Orchestration Flows
### Flow 1 — Add Slave CMS
```mermaid
sequenceDiagram
box rgba(99,179,237,0.3) Frontend
participant UI as Browser
end
box rgba(154,230,180,0.3) Master CMS
participant Ctrl as CmsInstanceController
participant Svc as CmsInstanceService
participant Repo as CmsInstanceRepository
participant Client as SlaveApiClient
end
box rgba(246,224,94,0.3) Slave CMS
participant SlaveEndpoint as AvailabilityController
end
UI->>Ctrl: POST /api/v1/CmsInstances
Ctrl->>Svc: AddAsync(request)
Svc->>Repo: AddAsync(entity)
Repo-->>Svc: entity tracked
Svc->>Repo: SaveChangesAsync()
Svc->>Client: RegisterMasterAsync(slaveUrl, apiKey, masterUrl)
Client->>SlaveEndpoint: POST /api/internal/master/register
SlaveEndpoint-->>Client: 200 OK
Client-->>Svc: true
Svc->>Repo: UpdateAsync (LastContactedAt)
Repo-->>Svc: updated
Svc->>Repo: SaveChangesAsync()
Svc-->>Ctrl: CmsInstanceDto
Ctrl-->>UI: 201 Created
```
Text alternative: Browser posts new slave to master controller → service persists → calls slave registration endpoint → updates LastContactedAt → returns DTO.
---
### Flow 2 — Set Slave Status
```mermaid
sequenceDiagram
box rgba(99,179,237,0.3) Frontend
participant UI as Browser
end
box rgba(154,230,180,0.3) Master CMS
participant Ctrl as CmsInstanceController
participant Svc as CmsInstanceService
participant Repo as CmsInstanceRepository
participant Client as SlaveApiClient
end
box rgba(246,224,94,0.3) Slave CMS
participant SlaveCtrl as AvailabilityController
end
UI->>Ctrl: PUT /api/v1/CmsInstances/{id}/status
Ctrl->>Svc: UpdateStatusAsync(id, status, disableMessage)
Svc->>Repo: GetByIdAsync(id)
Repo-->>Svc: CmsInstance
Note over Svc: Validate DisableMessage required if NotAvailable
Svc->>Repo: UpdateAsync (Status, DisableMessage)
Svc->>Repo: SaveChangesAsync()
Svc->>Client: PushStatusAsync(url, apiKey, status, disableMessage)
Client->>SlaveCtrl: PUT /api/v1/Availability/admin/status
SlaveCtrl-->>Client: 200 OK
Client-->>Svc: true
Svc->>Repo: UpdateAsync (LastStatusPushedAt)
Svc->>Repo: SaveChangesAsync()
Svc-->>Ctrl: void
Ctrl-->>UI: 200 OK
```
Text alternative: Browser sends status update → master validates, persists, pushes to slave endpoint → updates LastStatusPushedAt → returns 200.
---
### Flow 3 — Integrity Check (Background)
```mermaid
sequenceDiagram
box rgba(200,200,200,0.3) Background
participant Timer as PeriodicTimer
participant BgSvc as IntegrityCheckBackgroundService
end
box rgba(154,230,180,0.3) Master CMS
participant Svc as CmsInstanceService
participant Repo as CmsInstanceRepository
participant Client as SlaveApiClient
end
box rgba(246,224,94,0.3) Slave CMS
participant SlaveEndpoint as Slave API
end
Timer->>BgSvc: Tick (every IntegrityCheckIntervalMinutes)
BgSvc->>Svc: VerifyIntegrityAsync()
Svc->>Repo: GetActiveAsync()
Repo-->>Svc: list of active CmsInstances
loop for each active instance
Svc->>Client: GetRegisteredMasterUrlAsync(slaveUrl, apiKey)
Client->>SlaveEndpoint: GET /api/internal/master/registration
SlaveEndpoint-->>Client: registeredMasterUrl
Client-->>Svc: registeredMasterUrl
alt URL mismatch
Svc->>Client: RegisterMasterAsync(slaveUrl, apiKey, masterUrl)
Client->>SlaveEndpoint: POST /api/internal/master/register
SlaveEndpoint-->>Client: 200 OK
Client-->>Svc: true
end
end
Svc-->>BgSvc: done
```
Text alternative: Background timer triggers integrity service → per active slave checks registered URL → re-registers if mismatch.
---
### Flow 4 — Two-Phase Availability Gate (Slave Middleware)
```mermaid
flowchart TD
Start(["Incoming HTTP Request"])
Bypass{"Bypass prefix?"}
HasMaster{"MasterRegistration\nexists in DB?"}
CacheFresh{"Cache fresh?"}
PullMaster["Pull status from Master\n(HTTP GET)"]
PullOk{"Pull successful?"}
MasterStatus{"Master status?"}
LocalCheck["Existing local gate\nIAvailabilityService.IsAvailableAsync()"]
AdminBypass{"Admin bypass\n(Owner/Admin JWT)?"}
Block503Master["503 Service Unavailable\n+ DisableMessage"]
Block503Local["503 / Maintenance"]
Pass(["Pass request to next middleware"])
Start --> Bypass
Bypass -->|yes| Pass
Bypass -->|no| HasMaster
HasMaster -->|no| LocalCheck
HasMaster -->|yes| CacheFresh
CacheFresh -->|yes| MasterStatus
CacheFresh -->|no| PullMaster
PullMaster --> PullOk
PullOk -->|yes| MasterStatus
PullOk -->|no - use cached| MasterStatus
MasterStatus -->|NotAvailable| Block503Master
MasterStatus -->|Available| LocalCheck
LocalCheck -->|Available| Pass
LocalCheck -->|NotAvailable or Maintenance| AdminBypass
AdminBypass -->|yes| Pass
AdminBypass -->|no| Block503Local
classDef decision fill:#FFC107,stroke:#F57F17,stroke-width:2px,color:#000
classDef action fill:#63b3ed,stroke:#2b6cb0,stroke-width:1px,color:#000
classDef terminal fill:#9ae6b4,stroke:#2f855a,stroke-width:2px,color:#000
classDef block fill:#FC8181,stroke:#C53030,stroke-width:2px,color:#000
class Bypass,HasMaster,CacheFresh,PullOk,MasterStatus,AdminBypass decision
class PullMaster,LocalCheck action
class Start terminal
class Pass terminal
class Block503Master,Block503Local block
```
Text alternative: Request enters middleware → check bypass → if master registered check cached/pulled status → if NotAvailable return 503 with DisableMessage → else proceed to existing local availability gate.
---
### Flow 5 — Slave Registration (Master Registers Itself)
```mermaid
sequenceDiagram
box rgba(154,230,180,0.3) Master CMS
participant Client as SlaveApiClient
end
box rgba(246,224,94,0.3) Slave CMS
participant Ctrl as AvailabilityController
participant DB as AvailabilityDbContext
end
Client->>Ctrl: POST /api/internal/master/register\nHeader: X-Master-Api-Key\nBody: {masterUrl}
Ctrl->>Ctrl: Validate API key vs MasterModuleOptions.ApiKey
alt Invalid key
Ctrl-->>Client: 401 Unauthorized
else Valid key
Ctrl->>DB: Upsert MasterRegistration (masterUrl)
DB-->>Ctrl: saved
Ctrl-->>Client: 200 OK
end
```
Text alternative: Master posts registration with API key header → slave validates key → upserts MasterRegistration record → returns 200.