234 lines
9.5 KiB
Markdown
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.
|