Salta ai contenuti

0034. Durable Channex ARI publication and automatic repair

Questi contenuti non sono ancora disponibili nella tua lingua.

  • Status: Accepted, revised after R1 R2 and R3
  • Date: 2026-10-27
  • Replaces the initial connection-wide closure, quiet-period recovery latch and permanent unknown-send blockade.
  • Extended by: ADR 0050: publication reaches the workspace’s Channex environment, staging for a sandbox.

An administrator maps compatible base/derived plans, leaves direct-only plans unpublished, activates publication after booking reconciliation, and inspects local health even while Channex discovery fails. Saved mappings survive reload. Concurrent mapping writes require strong ETag/If-Match and stale writes return 412. Other roles inspect; only administrators configure, realign or acknowledge stuck publication. Every command is tenant-isolated and audited. Errors retain drafts and date errors link to fields in both English and Italian.

ADR 0036 preserves that built-in matrix while allowing custom delegation: health, mappings and upstream plan discovery require channex.view_health; mapping, resync and repair require channex.configure. Channex’s price limits in health also require rates.read for the property (ADR 0036, 30 October 2026). Selected-property members cannot use these workspace-wide Channex capabilities. The still-unapplied role migration follows this ARI chain as 0016.

Ledger changes and received/applied OTA bookings send current absolute availability for every affected date, including when it equals the last sent value. Forty changing prices edited sequentially across three ranges in 30 seconds coalesce into one restrictions call; two real imported bookings produce two priority availability calls. A received booking signal holds positive availability for its property while revisions are applied. Zero closures always pass. During a hold verified identical zeros are suppressed while the original changes remain pending for positive dates after settlement. A hold lasting 15 minutes enters explicit degraded closure, with a health alert naming the workspace administrator as the responsible operator. Unrelated properties continue publishing. Quiet periods never close a property or require manual takeover.

A lost response or expired sender cannot permanently stop publication. Identifiable tasks are checked; unknown packets settle after a bounded quarantine, then their dates are recomputed with the cache bypassed. Sold-out/stop-sell closures can pass during that wait. Administrators can see and request audited repair of stuck sends/rejected changes from Channels. All state survives restart. Each horizon date opens when its mapped plans have verified prices or verified stop sell; unrelated restriction tasks do not block it.

The assigned briefs substitute for a separate feature issue. Main’s e-mail migration 0012 is followed by unpublished ARI migrations 0013 (publisher), 0014 (repair) and 0015 (reopening). Renumbering preserves their SQL bytes. The generated manifest and real PostgreSQL catalog/isolation checks use the integrated contiguous chain 0001–0015.

Core owns the protocol and injected ports, clock and identifiers. The existing property writer gate, reservation ledger, effective rates and transactional outbox remain the source of truth. Database adapters implement exact-ID claims, forced RLS, composite tenant references, leases, pacing, retries, mapping versions and verified commercial state.

A numbered lease is acquired atomically. Every HTTP attempt is recorded before the call. Lease/heartbeat, pacing, retry deadlines, unknown settling and daily full-sync quotas use PostgreSQL time. An expired holder’s in-flight packets are adopted without its cooperation; a late task ID can settle its original record, but cannot finish another generation’s claims. A slow discovery response must revalidate the lease before changing metadata.

Availability never uses the last-sent delta cache: Channex automatically decrements OTA bookings, independently of GuestCraft. Prices/restrictions compare against verified daily values, merging partial fields. Explicit full-resync intent survives supersession and splitting. Closing sends zero availability and stop sell on every mapped plan. A safety state transition seeds closure/reopening; settled closure rows do not spin forever.

Availability gathers for six seconds. Commercial edits share a property window anchored on the oldest eligible pending commercial change (45 seconds); deferred beyond-horizon and capped rows cannot anchor the window. When it opens, newer pending edits join it. Priority booking and stop-sell changes use the fast path. Adjacent equal dates merge. Requests fit a 512 KiB budget, with durable date-window splitting. ARI POST deadlines scale from 7 seconds with serialized size, capped at 16 seconds; reads retain their bounded deadlines. The ARI job has a 90-second budget for discovery and both writes.

Each endpoint permits at most one call every six seconds per property, using database state without accumulated bursts. 429/5xx pause publication for at least a minute, with exponential backoff and Retry-After/RateLimit support. Item rejections back off from one minute to one hour; after six rejected attempts, automatic retries stop and health retains an operator issue. Administrator repair clears the retry cap and queues current values. A consumed wake-up during a pause or live lease clears its marker and schedules a durable continuation at the later database deadline, even if the holder has died. An earlier priority wake can replace a later scheduled continuation.

Verified webhook bodies route only the canonical property identifier; no upstream body or personal data enters signal storage, logs or action state. Signals advance a durable connection sequence and that property’s barrier generation. A signal without a routable property conservatively holds the connection’s properties. Requests and safety-drain starts do not raise barriers. An older drain cannot clear a newer signal.

The importer isolates each filtered feed infrastructure failure, visits healthy sibling properties in the same bounded drain, then reports the failure for retry. Failure-triggered recovery holds only properties with unresolved revisions; healthy siblings remain open. It drains signalled properties individually and settles each after its filtered feed is applied/quarantined, before unrelated backlog completes. Settlement transactionally wakes the ARI publisher. Quarantined/unknown room types and incompatible managed plans produce explicit health warnings and prudent closures. Local application/quarantine settles a property even if acknowledgement fails and the revision remains in the feed. Ack retries have a separate half-budget, cannot starve other properties, and do not initiate outage recovery. Failed acknowledgements still request a retry through the job result after local settlement commits; initial activation and full resync inspect intake failure separately. A fully locally processed feed clears feed-stage recovery despite failed acks. A normal barrier holds positive values only. Its first unresolved signal timestamp survives additional signals until a drain settles that generation. A partial settlement advances the remaining hold age to the successful drain, so continuous healthy intake does not escalate an old timestamp; after 15 minutes of database time an unresolved hold becomes degraded and seeds prudent zero/stop-sell closure. Health shows booking_feed_unavailable in both languages and assigns repair responsibility to the workspace administrator. The durable continuation/safety scheduler revisits the deadline. Local reconciliation clears the alert and seeds reopening. Alerts on the property and the webhook catalog are ADR 0048’s; individual incident assignment remains planned. Drain age alone is a monitoring signal. Silence is not evidence of downtime.

A resident restart with an old checkpoint initiates automatic recovery. Hosted deployments use the stable VERCEL_DEPLOYMENT_ID, persisted per connection, so a new deployment can initiate catch-up after an outage without treating every Function invocation as a restart. Sustained failed drains also initiate recovery. The durable history → future bookings → feed stages resume across workers. Historical revisions are scanned newest first to the last complete scan/last drain minus ten minutes; known IDs are skipped without ending the scan. Current/future bookings are reconciled through GET /bookings and their revision IDs. Pages contain at most five entries to keep transport calls inside the booking job budget. The final catch-up drain reopens automatically. It never consumes the manual full-sync quota or requires another takeover acknowledgement.

Initial takeover remains explicit: operators reconcile pre-existing bookings and revoke other writers. A successful empty feed alone does not prove initial historical completeness.

Known tasks are polled. Unidentified sends remain durable and visible for 120 seconds, measured from the persisted send timestamp using database time. After that bounded wait, the worker records bounded_repair, returns the original exact changes to pending, and recomputes current absolute values with commercial cache bypass. It never replays stored payload bytes. While waiting, positive values are held; zero/stop-sell closures are allowed. An administrator can request repair from health; the app records the acknowledgement and flag, and the worker performs the repair with worker-only send permissions.

This follows the review-authorized absolute-value repair alternative. It does not prove a remote packet cannot arrive after the quarantine. Channex has no provider fencing token; late remote application and the snapshot-to-unreceived-signal interval remain distributed ordering risks. Provider request lookup (ADR 0048) now settles before this wait ends the unknown sends whose values Channex holds; it never resends earlier, because nothing Channex lists is specific to one request. These limitations are distinct from a permanent publication blockade.

Queued and verified horizon watermarks are separate. Every missing horizon day is seeded, including after downtime, capped at min(500, state_length). Initial, reopening and newly entering horizon rows use per-plan/date price readiness: a verified rate or verified stop sell on each mapped plan allows that date’s room availability to publish. Already published prices satisfy readiness immediately; prices sent during this run qualify when their task is verified. No whole-row restrictions_done gate remains.

Accepted items populate the daily cache even when another item in the packet is rejected. A rejected price invalidates only its plan/date cache cells and enqueues a separate priority stop-sell-only write for those cells. Such safety writes never complete the price retry. Inventory-only restriction writes cannot reopen an unpriced plan. Availability completion and retries are independent of the commercial rejection cap. Other dates and plans reopen within the fast window, and administrator repair retries current prices and reseeds the horizon even without another local edit. A horizon seed cannot supersede an ordinary availability change. Safety transitions run before the quiet return, including quarantine resolution that changes no inventory.

Changes beyond the current horizon remain durable for later entry. Health pending counts cover eligible work within the queued horizon; capped failures remain visible as issues. Neither capped rows nor deferred rows cause repeated quiet discovery calls.

Only mapped local plans publish. Unmapped direct/corporate plans remain usable locally and appear as a health notice; adding one cannot block resync or recovery. Currency, sell mode, room type and inheritance are checked at mapping time and before publication. Live mappings are additive; a live target is retired or replaced only through the verified workflow of “Live target retirement” below. Discovery reads are cached for a minute and reserve a database slot to coalesce concurrent member loads. Channex settings are shown with translated names/values and warnings, never automatically rewritten. Live discovery failure affects the mapping section only.

Health refreshes every 15 seconds and includes pending counts, sends, warnings, rejection attempts/deadlines, drain staleness, unpublished plans, lease, pause, barriers and horizon. API revision 1.12.0 adds versioned mapping reads and audited repair, updates health and requires strong If-Match on PUT /mappings. OpenAPI, SDK and query factories are generated.

On Vercel production, committed outbox writes and verified signals wake the same worker through Next.js after()/waitUntil. Async-local request state and after-commit notifications prevent rollback/read wake-ups. The route permits 300 seconds; the pump uses at most 240 seconds minus request time and reserves full job/cleanup headroom. Fluid compute is required. Docker retains its resident worker. Development, previews and restored/unowned instances cannot publish (ADR 0022).

A publisher schedules continuation only for pending changes, accepted task polling, bounded unknown repair or a real pause deadline. Quiet publishers do not self-schedule. A request pump waits only when the queue contains real delayed work within the remaining budget and coalescing window; it ends when work is exhausted. Horizon/safety schedules remain durable. Daily Hobby-compatible cron is a demo recovery safety net. An undrained booking signal or stalled deployment recovery closes the affected property on all mapped channels after 15 minutes; a daily cron cannot prevent this after a killed pump or redeploy. A commercial hosted installation requires a 1–5 minute Pro cron or external HTTPS scheduler: backoffs, unknown repair, missed webhooks and killed functions can exceed the request pump’s wait window. Longer pauses and missed requests still need another invocation. Scheduler 202 acknowledges scheduling, not completion.

R1 and R2 responses map every finding to changes and regressions. All seven reviewer probes were converted to assertions and failed against the reviewed publisher before passing after repair. Database tests move persisted timestamps where production reads database time. Real PostgreSQL/PgBouncer, the independent simulator, webhook/pg-boss integration, production browser workflows and accessibility checks cover the supported workflow; test counts do not measure product completeness.

Repeated unknown outcomes still use the bounded 120-second repair cadence without consecutive-timeout incident escalation (R2 L1, deferred). Their unknown/bounded-repair send history remains visible. Exceptionally late packets remain the accepted R2 L2 risk. Webhook routing verifies the secret before lazily reading up to 256 KiB, retaining only the validated identifier; larger or invalid deliveries conservatively remain unrouted.

Nightly remote drift repair, provider request lookup and alerts are implemented by ADR 0048. Live target retirement and sales caps are implemented (see the sections below and ADR 0027), as is occupancy pricing; real Channex certification remains planned. Hosted execution cannot guarantee work while no Function/scheduler runs. See the workflow contract and the provider booking collections.

R3 response maps F1, N6 and L1–L4 to regressions and verification. ARI/import clocks and simulator inputs are fixed; stay dates derive from those clocks. PostgreSQL deadline tests shift relative persisted instants, and elapsed worker/transport timing uses a monotonic clock. Built-browser fixtures and the test-only server preload share one injected instant without freezing worker/authentication timers. Existing acknowledgement failures remain visible per revision; acknowledgement failures are aggregated per property in ARI health and raise a sync failure alert (ADR 0048). Repeated-unknown escalation remains planned.

The HTTP runtime passes its clock into request-pump and cron workers; resident worker composition accepts the same explicit clock. Integration tests no longer fall back to the host date for worker horizons. Database deadlines remain separate from business dates: ARI acquisition/preparation enforce pauses, and the merged mail delivery scan uses database time just like the claim. Pre-arrival dates retain the injected clock.

ADR 0028 defines account modes, booking consumption, property linking, webhooks, recovery and takeover limitations.

ARI call-site audit: core/channex/ari.ts obtains a workspace property mapping before plan/settings discovery, setup, health or publication. Payloads are computed from mapped local room types and explicitly mapped compatible rate plans; availability/restriction writes contain that mapping’s remote property ID. Property/channel rules and plan lists use that same property filter. Settings are inspected, never rewritten. Task IDs come only from the mapped property’s persisted sends. Core, adapter and simulator tests prove that an unrelated property is neither discovered by ARI nor changed. Main has no Channex message operations. Exclusive booking consumption does not broaden ARI publication.

ADR 0028 defines audited unlink and retryable webhook cleanup. Unlink disables publication, revokes the numbered lease, retires queued rows without claiming upstream delivery, removes rate/room mappings and caches, and retains send diagnostics. The publisher checks its lease immediately before a send and before accepting its result. A later confirmed link requires fresh mappings and explicit takeover. Already transmitted writes cannot be recalled; unlink sends no compensating availability or price updates.

The pump classifies a failed run (ADR 0008). A retryable failure pauses the property with backoff and Retry-After, as before; a failure whose outcome is unknown keeps the schedule, because the job runner retries it. Any other failure stops the property: a non-retryable Channex failure (refused key or property, unusable answer) as upstream_rejected, a bug or another permanent failure as internal_failure. An unsupported Channex property currency is an expected outcome of the settings read and stops it as currency_unsupported; a mapping write answers channex.currency_unsupported (409). The error still reaches the job runner, which dead-letters a bug or permanent failure; the lease is always released.

Rate-plan discovery and a mapping replacement read Channex for the administrator. A non-retryable Channex failure of those reads answers channex.mapping_read_refused (409): the key or the property is the administrator’s to fix. A retryable failure still answers server.unavailable (503, with Retry-After when Channex gave one) and a bug server.internal.

Migration 0026 (expand) adds the nullable ari_properties.stop_code and resumed_generation. recovery_required was not reused: it means a lost ownership that needs a takeover full resync, which has a daily quota. pause_until lapses by itself, and degraded closes sales. A stopped property is not leased, woken or scheduled: acquire, request (the gate of every enqueue: rate changes, booking signals, the minute’s safety pass, the pump’s continuation) and nextDelay refuse it. Its pending changes, accepted and unknown sends stay durable and are settled or repaired after the resume. Health reports the stop as a property-level issue with retry_at null; Channels shows it as Publication has stopped with the recovery. A requested repair, an enabled publication (resync or takeover) or a saved Channex connection clears the stop and records the lease generation it happened in as resumed_generation.

A worker stops the property only while its lease holds it (same generation and lease_owner) and only if no administrator resumed the property during that lease: its read may have failed before a repair or a new key, and a stale stop would silently undo the resume. The lease alone does not show a resume, because recovery does not take the lease from a running worker: doing so, as unlink does, would let a second worker publish while the first one’s send is still in flight. A worker refused this way releases as usual; its continuation runs the property again, and the next lease stops it if the cause persists. API revision 3.8.0 documents the codes.

ADR 0027 adds a plan’s sales cap (maxSell). The publisher sends its remaining count, max(0, cap − sold), as max_sell (OpenAPI Restrictions.RestrictionUpdate.max_sell: “Maximum rooms that may be sold on the date. null removes the limit”):

  • Channex does not document whether max_sell counts the bookings Channex itself receives. The remaining count never oversells under either reading: exact if it does not, an under-sale of the OTA rooms counted twice if it does. Sending the cap itself would oversell by the direct sales Channex never sees. This is listed for the certification session.
  • Every full restriction write of a mapped plan carries max_sell, null without a cap, so a stale limit cannot survive; the published cache and the drift read (GET /restrictions with max_sell in filter[restrictions]) keep null as a value, and a difference is repaired like any field. A plan with nothing left is also sent stop_sell: true, for channels that would not apply max_sell, and counts as closed when deciding whether its room type’s availability closes. A withheld plan is still sent only its stop sell.
  • Saving a reservation and importing a revision queue a rates change with the only field sales per plan whose sales change, so a plan switch on unchanged room-type inventory republishes both plans. Such a change sends only the plan’s stop sell and, for a capped plan, its remaining max_sell, never its prices or other restrictions, so a booking keeps certification scenario 9’s single closure call; like a cap edit (maxSell), it takes the fast path and also refreshes availability (the database’s fast-path fields list both with stopSell and restrictions). An inventory change of the room type republishes its plans’ remaining max_sell with their stop sell.
  • Channel availability rules (availability_offset, max_availability, close_out) stay read-only and are never folded into the sold count.

A live mapping cannot simply be removed: an OTA keeps selling whatever Channex last held for a target GuestCraft no longer publishes. Removing or changing a mapping while publication is enabled therefore answers channex.ari_mapping_live, and an administrator (channex.configure, workspace-wide, connection ownership, the bounded property lock) retires one live target at a time instead: a mapped rate plan, or a mapped room type with every plan on it. The action is unmap, or replace by another Channex rate plan (of the Channex room type the local room type is mapped to) or another Channex room type with a new plan list for the local plans of that type (plans may be dropped). A replacement must exist in Channex, not be mapped, and be publishable exactly as a mapping save requires (mappingIssue): a local setting it cannot publish names its field (rate_plan.not_distributable), anything else answers channex.ari_replacement_invalid with not_found, mapped or incompatible. The last mapped room type cannot be unmapped (channex.ari_retirement_last_room_type): that is an unlink.

Preview and start. validate_only answers, without writing, what the stop covers: the target, the local names, the nights [property-local today, horizon end), the Channex rate plans sent a stop sell and the room type sent 0, the open sends, the imported channel bookings still to depart that the target sold, and whether the target is live; one that is not is changed with the ordinary mapping edit. A start takes the mapping’s strong version (If-Match), refuses a second active retirement (channex.ari_retirement_active, a partial unique index), records the retirement and queues a priority change of the target’s room type over the horizon, marked for a full resend so that a stale published cache cannot skip the closure. Mapping edits are refused while a retirement is active.

States. stopping (queued) → draining → verifying → switching (replacement only) → completed, plus failed with a stable code and cancelled. Every state but completed and cancelled is active. The publication run moves the retirement on after its sends, under its lease and locks, like the drift check:

  1. While active, the target joins the withholding: its mappings are stop-sold, a stop sell without prices or other restrictions. A room type whose every plan is withheld closes its availability, so a retired room type is sent availability 0 and each of its plans an explicit stop_sell: true; a retired plan whose room type keeps healthy plans is stop-sold alone. A stop sell now wins over a plan issue’s room-type closure (an inherited plan is sent one too); a mapping a plan issue sends nothing (its Channex plan does not exist) stays unsent, and that plan counts as closed (see step 3). GuestCraft never sends stop_sell: false to reopen a zero-availability plan and never writes Channex’s channel availability rules; the closure is the explicit stop sell, which closes a plan whatever its max_sell, so no max_sell is needed to prove it. A retirement in progress keeps the property’s runs coming (isQuiet is false); a failed one keeps its target withheld until an administrator acts.
  2. draining waits until the closure is settled: no booking hold, no open send (isOpenSend, so an unknown send is first looked up or repaired) and no change of the target’s room type left to publish before the queued horizon, other than one Channex rejected past the retry limit. A task acknowledgement or a local queue state is never taken as proof.
  3. verifying reads Channex back over [today, queued horizon) in 92-night windows (GET /availability, GET /restrictions): the stop-sold plans’ explicit closure (stop_sell_manual, which the adapter exposes as stop_sell) on every night, and for a room type availability 0 on every night. An absent value is not a closure. A booking signal, an open send or a mapping change since the reads discards them, back to draining without using an attempt. A target Channex no longer lists among the property’s plans or room types sells nothing there and counts as closed; one it still lists but returns nothing for fails the retirement as target_missing; differing nights are forgotten from the published cache, queued again and waited for (back to draining, one attempt used); the third such verification fails it as readback_mismatch. A temporary Channex failure is read again by the next run; a refusal fails it as upstream_rejected.
  4. A closure held on every night switches the mapping in one transaction under the gate and lease: the property’s plan mappings without the target (or with its replacement), the room mapping moved or removed, a new mapping version, the retired Channex targets (a retired room type fences its plans too) recorded in channex_retired_targets, their published cache rows deleted so that the drift check never compares them again, and, for a room mapping change, the property’s quarantined unknown_room_type and retired_target receipts re-armed for one more automatic replay, as a mapping save does through a new connection version (the worker cannot lock the workspace row that a connection update locks). An unmap completes there.
  5. A replacement is then switching: the same local mappings, now naming the replacement, stay withheld, so the replacement is first sent a stop sell and a replacement room type availability 0; never both targets open, and the interval in which neither sells is visible in health. Once Channex has applied those values (acknowledged tasks, not a read-back) on every night and nothing of the room type is left to publish, the retirement completes, the room type is queued again and the replacement publishes its rates and restrictions; its availability opens only through the existing price-readiness rule. If nothing is left to publish while those values are still missing, as when Channex rejects them past the retry limit or the replacement was deleted in Channex, the retirement fails as replacement_unpublished, the replacement still closed.

Cancel and resume. Cancel is allowed before the switch (stopping, draining, verifying, failed): the target leaves the withholding and its room type is queued again over the horizon; the closure already sent stays until that reseed is published. Cancel is also allowed for a failed switch: it keeps the switched mapping and the replacement publishes normally. Otherwise, once the mapping switched, it cannot be cancelled, and a completed retirement cannot be undone. Resume after replacement_unpublished returns to switching and sends the replacement’s values again; any other failure resumes from stopping with its attempts reset and its closure resent. Starts, cancels and resumes are audited with the acting principal (channex.ari_retirement_started|cancelled|resumed); the run’s transitions are system-audited (…_mismatch, …_failed, …_switched, …_completed), each with its domain event. A failed retirement opens the sync_failure alert with the cause retirement_failed (ADR 0048). Unlinking the property cancels its active retirement. Health shows the active retirement, or the latest one.

Bookings of a retired target. A revision that names a retired Channex room type is not projected as a normal amendment, and never into the replacement: it is quarantined as retired_target, acknowledged and replayed automatically once per connection version exactly like unknown_room_type (ADR 0028), its encrypted archive kept. The administrator resolves it with an audited retry that applies it to the retired target’s recorded local room type, or maps the Channex room type again, which removes its fence, and retries. The fence is keyed by the workspace and the Channex identifier, like migration 0024’s retired properties, so relinking the local property elsewhere does not erase it. A retired Channex rate plan keeps resolving to the local plan it was sold under (plan identity retention), never to its replacement, so a booking sold on it still imports with its plan and sold counts.

Migration 0073 (expand) adds ari_retirements and channex_retired_targets, both with composite foreign keys cascading from the property, forced workspace and property RLS and the workspace_active archival policy; the worker gains the grants the switch needs. API revision 6.9.0 adds POST /properties/{property_id}/channex/ari/retirements (preview with validate_only, or create), GET …/retirements/{retirement_id}, POST …/cancel and POST …/resume, and health’s retirement. The simulator proves the protocol; a live check of the closure with Channex staff is part of certification.