L1: an artifact apply holding the slot past 10 min (ctor-overridable) logs an
Error site event naming it without releasing the slot; PostStop answers
queued applies Failed. L2: a boot-batch instance whose actor cannot be created
emits an Error site event and leaves the deployed set. L3: a failed enable
reverts the committed is_enabled (generation-bumped both ends). L4: the routed
SetAttributes batch Ask is the 30 s per-write bound plus 10 s mailbox slack.
ConnectionActorNames.Sanitize (shared by the DCL manager and the site deploy
gate) keeps ASCII letters/digits and -_.*+:@&=,!~';() only. The manager fails a
connection whose sanitised name is invalid or already used (Error + site
event) instead of restarting; DeployCompileValidator rejects a config whose
connection names collide after sanitising.
HandleOverridesLoaded now sends children a site-local AttributeValueSeed
(stream still gets the AttributeValueChanged). ScriptActor updates its
snapshot, Conditional truth value and Expression last result (seed-only eval,
never fires); AlarmActor evaluates it as a normal change.
DeployCompileValidator.WarmCompileCache dedupes script bodies/trigger
expressions across a batch and compiles them with Parallel.ForEach (half the
cores). CreateInstanceActorBatch starts the next batch's warm as it arms the
stagger timer; creation order and delay are unchanged. The warm's parsed
configs are reused at creation.
The deploy compile warm parses once off-thread; the parsed FlattenedConfiguration
rides DeployCompileWarmed to the gate (new Validate(FlattenedConfiguration)
overload), the redeploy buffer, CreateInstanceActor's DCL ensure and a new
InstanceActor constructor overload. Stored-row paths parse once inside
CreateInstanceActor; an unparseable stored row still fails init via S6.
Adds SiteStorageService.GetAllDeployedConfigMetadataAsync. The sweep decides
from the projection, then fetches single rows for its starts/restarts off-
thread and re-validates them (generation guard, revision match, pipeline/
running guards), releasing the per-revision claim of any repair that does
not act. Boot load unchanged.
One Ask per attribute became one site-local InstanceGet/SetAttributesRequest
answered in a single pass, keeping the per-attribute GetAttributeResponse /
SetStaticAttributeResponse outcomes. The single-write path is refactored to a
Task<SetStaticAttributeResponse> outcome shared by both paths; static writes
and rejections still reply synchronously.
Each apply upserts its full set then deletes everything else, off the actor
thread; two overlapping commands interleaved and deleted artifacts central
never removed. The Deployment Manager now runs one apply at a time, queuing
the rest in arrival order; the slot is released by a self-message that lands
behind the apply's own DCL push.
SR-U1: children are created before the async override load lands, so their
trigger snapshots kept the deploy default forever after a restart/failover/
enable/sweep start. HandleOverridesLoaded now republishes every changed value
through PublishAndNotifyChildren(evaluateWaiters:false), and skips attributes
a static write touched before the load (newer than the stored row).
SR-M3: PostStop cancels each WaitForAttribute timeout and replies
'instance stopped' instead of leaving scripts to time out and timeouts to
dead-letter.
An instance unique name Akka refuses as a path element (space, '#', '/',
non-ASCII) made Context.ActorOf throw on the singleton's own thread; the
default Restart stopped every Instance Actor on the site. DeployCompileValidator
now checks the instance + script/alarm/native-source actor names with
ActorPath.IsValidPathElement (the Commons ActorNameRules mirror is stricter
than Akka for " ( ), pinned by a parity sweep), CreateInstanceActor catches
InvalidActorNameException and every caller reports it, and the Instance Actor
skips a refused child name like a compile failure.
- CreateDefaultHttpClient sets AllowAutoRedirect=false so a 307/308 cannot re-POST the
/auth/token credential form elsewhere.
- A forced re-exchange after a 401 carries the refused token; inside the gate it reuses
a token another caller already re-exchanged instead of binding again. The BasicOnly
switch is written under the same gate.
- SCADABRIDGE_VERBOSE=1 prints a stderr note when a fresh token is refused and the CLI
falls back to Basic.
- debug stream's AccessTokenProvider retries the exchange once and documents the
null-token degradation (the hub refuses, automatic reconnect retries).
- Tests: concurrent refusal reuses the peer refresh (2 exchanges, not 3); real-socket
redirect is not followed.
- JwtTokenService back-dates nbf by the 30 s validation skew so a peer central whose
clock lags the issuer accepts a fresh token after failover.
- RoleMappingCacheSeconds = 0 routes RoleMapper to the matched-rows loader (old cost)
instead of loading every Deployer mapping's scope rules per call.
- RoleMappingCache / SecurityOptions / Component-Security.md describe Invalidate() by
its contract plus the TTL, not by a call site.
- Tests: HS512 with the same key, issuer/audience mismatch, lagging-peer nbf, TTL-0
matched-only reads, refresh-vs-Invalidate generation race.
- ManagementAuthenticator accepts Authorization: Bearer <jwt> alongside Basic on
/management, the audit REST endpoints and the debug-stream hub. Tokens are
validated locally (HS256 only, alg=none rejected, issuer/audience, lifetime with
30 s skew); roles and site scope inside a valid token are authoritative for its
lifetime, so a bearer call costs no LDAP bind and no role re-map.
- /auth/token returns expires_in + management_bearer:true, carries username, display
name, roles and permitted sites, and answers 404 under DisableLogin.
- CLI exchanges credentials once per process (in-memory CliTokenStore), sends Bearer,
re-exchanges once on 401 then falls back to Basic; older/login-disabled servers are
Basic-only; bad credentials are not retried with Basic. Debug stream uses the token
via AccessTokenProvider. SCADABRIDGE_AUTH_MODE=basic opts out.
- StartupValidator fails a Central boot on a JwtSigningKey under 32 UTF-8 bytes;
fixed the false 'fail-fast at construction' comment. Removed the dead
RefreshToken/RecordActivity/ShouldRefresh/IsIdleTimedOut members and the inert
JwtRefreshThresholdMinutes; JwtExpiryMinutes now documented as the bearer lifetime
(validated > 0).
- RoleMapper reads through a process-wide RoleMappingCache (RoleMappingCacheSeconds,
default 30 s): one refresh loads the table plus every Deployer mapping's scope
rules, Invalidate() drops it (generation-guarded against in-flight refreshes).
- Component-Security.md / Component-CLI.md / CLI README describe the real model
(cookie claims ticket, bearer for CLI) instead of a cookie-embedded JWT.
InstanceService.SetAlarmOverrideAsync and TemplateDeletionService move to the
no-tracking, body-free GetAllTemplatesForAnalysisAsync; TemplateFolderService
counts folder contents from GetTemplateStubsAsync.