feat(routes): live group-route expansion + full-live invalidation (§11 tail R3)
The runtime half. Group route TEMPLATES now expand into every descendant app's in-memory match slice, live, with no per-app materialization. - `compile_effective_routes` consumes the `list_effective` expansion and applies NEAREST-OWNER-WINS shadowing: for one app, identical binding tuples (method+host+path) owned at multiple chain levels collapse to the nearest (an app's own route shadows an ancestor-group template); a nearer group beats a farther one. Non-identical bindings coexist and the existing matcher precedence resolves each request. `compile_route` now takes the effective app_id, so the matcher + dispatch are unchanged. - `rebuild_route_table` is the single refresh chokepoint (list_effective → compile_effective_routes → replace_all). Route CRUD, the apply reconcile, and startup all route through it; the apply guards drop the "a group node touches no routes" assumption (a group apply may now create templates). - FULL-LIVE INVALIDATION: app create/delete (apps_api) and group reparent (groups_api) rebuild the snapshot, so inherited routes take effect the instant the tree changes — matching the live model of triggers/vars. Best-effort, mirroring apply: a failed rebuild self-heals on the next write or restart, never failing the committed mutation. The isolation boundary is the chain expansion: a template lands only in the slices of apps whose ancestor chain contains the owning group. Pinned by a deterministic live-DB integration test (descendant inherits, sibling subtree does not, app shadows by identical binding, non-identical coexists). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,9 +11,9 @@ use axum::{routing::get, Json, Router};
|
||||
use picloud_executor_core::{Engine, Limits};
|
||||
use picloud_manager_core::{
|
||||
admin_router, admins_router, api_keys_router, app_members_router, apply_router, apps_api,
|
||||
apps_router, attach_principal_if_present, auth_router, compile_routes, dead_letters_router,
|
||||
dev_emails_router, email_inbound_router, files_admin_router, groups_router, kv_admin_router,
|
||||
migrations, require_authenticated, route_admin_router, secrets_router, topics_router,
|
||||
apps_router, attach_principal_if_present, auth_router, dead_letters_router, dev_emails_router,
|
||||
email_inbound_router, files_admin_router, groups_router, kv_admin_router, migrations,
|
||||
rebuild_route_table, require_authenticated, route_admin_router, secrets_router, topics_router,
|
||||
triggers_router, vars_router, AbandonedRepo, AdminPrincipalResolver, AdminSessionRepository,
|
||||
AdminState, AdminUserRepository, AdminsState, ApiKeyRepository, ApiKeysState,
|
||||
AppDomainRepository, AppMembersRepository, AppMembersState, AppRepository, ApplyService,
|
||||
@@ -33,9 +33,9 @@ use picloud_manager_core::{
|
||||
PostgresOutboxRepo, PostgresPubsubRepo, PostgresRouteRepository, PostgresScriptRepository,
|
||||
PostgresSecretsRepo, PostgresTopicRepo, PostgresTriggerRepo, PostgresVarsRepo,
|
||||
PrincipalResolver, PubsubServiceImpl, RealtimeAuthorityImpl, RepoResolver, RouteAdminState,
|
||||
RouteRepository, SandboxCeiling, ScriptRepository, SecretsConfig, SecretsServiceImpl,
|
||||
SecretsState, SubscriberTokenConfig, TopicRepo, TopicsState, TriggerConfig, TriggerRepo,
|
||||
TriggersState, UsersServiceConfig, UsersServiceImpl, VarsApiState, VarsServiceImpl,
|
||||
SandboxCeiling, ScriptRepository, SecretsConfig, SecretsServiceImpl, SecretsState,
|
||||
SubscriberTokenConfig, TopicRepo, TopicsState, TriggerConfig, TriggerRepo, TriggersState,
|
||||
UsersServiceConfig, UsersServiceImpl, VarsApiState, VarsServiceImpl,
|
||||
};
|
||||
use picloud_orchestrator_core::realtime::DEFAULT_GC_INTERVAL_SECS;
|
||||
use picloud_orchestrator_core::routing::{AppDomainTable, RouteTable};
|
||||
@@ -328,12 +328,12 @@ pub async fn build_app(
|
||||
// route_repo, then re-populated whenever the admin layer writes
|
||||
// routes.
|
||||
let route_table = Arc::new(RouteTable::new());
|
||||
let initial = route_repo.list_all().await?;
|
||||
// Lenient: a single un-compilable stored route (e.g. one whose path
|
||||
// became reserved under stricter validation) is skipped-with-warning
|
||||
// inside compile_routes, never aborting boot. (H1)
|
||||
let compiled = compile_routes(&initial);
|
||||
route_table.replace_all(compiled);
|
||||
// §11 tail: rebuild via the live expansion so group route TEMPLATES land in
|
||||
// each descendant app's slice at boot. Lenient: a single un-compilable
|
||||
// stored route (e.g. one whose path became reserved under stricter
|
||||
// validation) is skipped-with-warning inside the compile, never aborting
|
||||
// boot. (H1)
|
||||
rebuild_route_table(route_repo.as_ref(), &route_table).await?;
|
||||
|
||||
// v1.1.9 function composition. InvokeServiceImpl resolves targets
|
||||
// by Id, Name (via get_by_name), or Path (via the orchestrator's
|
||||
@@ -555,8 +555,9 @@ pub async fn build_app(
|
||||
let apps_state = AppsState {
|
||||
apps: apps_repo.clone(),
|
||||
domains: domains_repo,
|
||||
routes: route_repo,
|
||||
routes: route_repo.clone(),
|
||||
domain_table: app_domain_table.clone(),
|
||||
route_table: route_table.clone(),
|
||||
authz: authz.clone(),
|
||||
groups: groups_repo.clone(),
|
||||
};
|
||||
@@ -604,6 +605,8 @@ pub async fn build_app(
|
||||
apps: apps_state.apps.clone(),
|
||||
users: auth.users.clone(),
|
||||
authz: authz.clone(),
|
||||
routes: route_repo.clone(),
|
||||
route_table: route_table.clone(),
|
||||
};
|
||||
let vars_state = VarsApiState {
|
||||
vars: Arc::new(PostgresVarsRepo::new(pool.clone())),
|
||||
|
||||
Reference in New Issue
Block a user