Adds 2 units and docs for unit 3. nfr-requirements plan
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user