Skip to main content

Shared releases

With synchronized contexts, every application of a split content model previews the others' drafts, releases and experiments, but only the application that manages the releases changes its entities in them. Shared releases let the other applications participate: a PIM changes its categories in the CMS's release autumn, with the same release id, and the CMS publishes the release with its pages and the PIM's categories together.

ApplicationRoleChanges in the release
CMSCoordinates its releases and experimentsIts pages
PIMParticipates in the CMS's releases and experimentsIts categories
DeliveryConsumes bothNothing

Each application merges only what it authors, in its own transaction. No transaction spans the applications, so a shared publication is atomic per application and eventual across them: Vox prepares every participant before any of them publishes, and it reports a partial failure as one, but it can't undo what an application has already published.

Roles​

UseContextSynchronization has three roles, configured per application:

  • Consume a source's contexts, see synchronized contexts. A delivery application needs nothing else, and it's never a participant.
  • Participate in a source's releases, or in a mergeable context type it explicitly shares, eg its experiments. It needs authoring, and it consumes the source's contexts too.
  • Coordinate the releases and mergeable context types this application owns with the configured participants. It needs authoring, with content releases if they're shared. The coordinator can change its own entities in them too.
// The CMS owns the releases and the experiments, and shares them with the PIM
services.AddVox(options => options
.UseAuthoring()
.ConfigureContextType<Experiment>(type => type.Mergeable())
.UseContextSynchronization(sync => sync
.Consume("pim", pim => pim.Drafts().Entities<Category>())
.Coordinate("cms", cms => cms
.Participant("pim", pim => pim
.At(new Uri("https://pim.example.com/vox/"), configuration["Vox:PimSecret"]!)
.Releases()
.ContextType<Experiment>()))));

// The PIM changes its categories in them, without releases of its own
services.AddVox(options => options
.UseAuthoring(authoring => authoring.WithoutContentReleases())
.ConfigureContextType<Experiment>(type => type.Mergeable())
.UseContextSynchronization(sync => sync
.Participate("cms", cms => cms
.Drafts()
.Releases<CmsRelease>()
.ContextType<Experiment>()
.Entities<CategoryContentPage>())));
  • Coordinate(source, ...) names this application as its participants know it, the source of the snapshots it sends them. Participant(name, ...) adds a participant by the name of the source of its snapshots. At(endpoint, secret) is the base address of the participant's Vox API and the secret its requests are signed with; register another IVoxContextParticipantClient to reach the participants in another way.
  • Participate(source, ...) takes what's shared: Releases<TRelease>() binds the source's releases, and ContextType<TContext>() a mergeable context type of the source, which keeps the source's name, as the generated external type does, since the participant's contributions are keyed by it. The source's requests are signed with the secret of its snapshots, configured with apiOptions.ReceiveEntitySync(source, secret).

The coordinator also receives the participants' contributions: a snapshot subscription of the PIM that chooses $release and the shared context types in SidecarContextTypes carries the PIM's changes in the CMS's releases to the CMS, which previews and validates its pages against them.

Startup rejects a participant without authoring or without anything shared, a coordinator without authoring, releases shared by an application without releases of its own, and a shared context type that the coordinator doesn't own.

Changing entities in a shared release​

A participant's authored entities are changed in the source's release like in a release of its own: saved at its key, $release=autumn, with IVoxAuthoringEntityUpdater, added and removed with IVoxContentReleaseManager, and previewed with context=$release:autumn. The release must be open, which its synchronized status tells. A participant's item operations need its own publish permission of the entity type, since the release isn't managed there, while creating the release, scheduling, publishing and discarding it belong to the coordinator.

Each application lists only its own items. Its merge never edits or deletes the release or the coordinator's experiment, and never merges the cells of external entities: the CMS's cells of the PIM's category in the release stay until the PIM's snapshots remove them.

Publishing​

IVoxContentReleaseManager.PublishAsync of a shared release, or its due schedule, starts a coordinated operation, which has a stable id, the release's identity and lifecycle revision, the participants with their states, and their receipts. It's saved before every request, so a restart or a lost acknowledgement continues it where it was.

  1. Prepare. Every participant, the coordinator's own contribution first, closes its contribution with a durable guard and captures it: the entities it changes, with the revisions of their snapshots. While it's closed, every write of a local authored entity at the release's key is rejected, also entity, batch and cell updates, existence, imports, publishing drafts into the release and removing items. Synchronization and calculations continue.
  2. Validate. Every participant waits until the others' captured entities have arrived there, at least at the captured revisions, so it never validates against a copy it knows is stale. Then it validates its contribution, including its relations to the others' content in the release, with the rules of a release: drafts within the release, conflicts with published content, publish permissions and validation.
  3. Commit. Once every participant has prepared, the commit is decided. Every participant merges its prepared contribution and records its receipt in one transaction, and a participant without changes records a no-op. A release with content in any application can be published; one with no content anywhere is rejected.
  4. Finish. Once every participant has committed, the coordinator marks the release published. Receiving that status is never an instruction to merge. Normal synchronization carries the published content, and removes the preview cells.

A participant that fails before the commit cancels the operation: the others reopen their contributions, nothing is published, and the release gets the error, like a failed publication of a release of one application. After the commit decision the operation can't be cancelled: a participant that fails to commit, eg on a conflict with what was published since, reopens its contribution for repair and is retried, while what the others committed stays committed and can be visible. An unavailable participant leaves the operation pending, never silently excluded.

PublishAsync returns the published release when every participant answers at once. When the operation continues in the background, eg until an unavailable participant has prepared, it throws VoxContextOperationPendingException with the operation, and the API answers 202 with it. The operation continues on the Nexus queue vox_context_operation.

Retries and idempotency​

Every request is retried with the operation id until it's answered. A participant that has committed returns its receipt instead of merging later edits or publishing again. A delayed request of a cancelled or superseded operation, recognized by its lifecycle revision, is rejected, and so is a request with another payload than the one recorded for its id. Only one operation of a member runs at a time, and a manual action and a schedule compete through it: an obsolete schedule is a no-op once the release is published, and changing the schedule while an operation runs is rejected.

Schedules and reverts​

Only the coordinator schedules. A due schedule starts the same operation as publishing now, and participants run no timers of their own. A shared revert is an operation of its own, tied to the receipts of the publication it reverts: every participant checks that its own publication can be restored, then restores it atomically, conflicting as restores do, and the release is marked reverted once all have. A release published before it was shared is reverted where it was published.

Merging and discarding​

Merging a variant of a shared experiment with IVoxContextMerger runs the same protocol, and the coordinator deletes the variant once every participant has merged its contribution.

Discarding a release or a variant that isn't published, with IVoxContextOperations.DiscardAsync or the ordinary delete of its entity, closes every participant's contribution, asks each one to remove it, and then deletes the member at the coordinator. A published release can't be discarded; revert it instead.

A deleted member is retired with how it ended, Merged, Discarded or Deleted, the operation that ended it and its lifecycle revision, and its deletion carries that to the participants and consumers, since neither the member's absence nor its deletion tells what became of the contributions. A participant holds back the deletion of a member while its own contribution there isn't settled, eg one the source says it merged while it wasn't merged there, instead of deleting it.

Operations in the API and the admin​

EndpointApplication
GET api/context-operations?contextType=$release&memberId=autumnCoordinatorThe operations of a member, with each participant's state, receipt and error
GET api/context-operations/{id}CoordinatorOne operation
POST api/context-operations/{id}/retryCoordinatorAsks the participants again
POST api/context-operations/{id}/cancelCoordinatorCancels an operation before its commit is decided
POST api/context-operations/discard?contextType=…&memberId=…&version=…CoordinatorDiscards a member
GET api/context-operations/contributions?contextType=…&memberId=…ParticipantIts own contributions
POST api/context-operations/{source}ParticipantThe signed requests of the source

Operations of releases need vox:ContentReleases at the coordinator. A participant authorizes the source's requests as the principal of the source, by default the integration vox-sync:{source}, which needs the publish permissions of the entity types it changes in the shared members; the user who started the operation is recorded, but a remote user's permissions don't apply. Change the principal with cms.As(principal).

The coordinator's release shows the participants it's shared with and the latest operation, with the progress and error of each participant, to retry or cancel it. A participant lists the source's releases with their status and the state of the source's operation, and shows its own entities in a release next to its part of the operation. A consumer stays read-only.

Eventual visibility​

A shared publication is visible application by application: the PIM's categories can be published a moment before the CMS's pages, and a delivery application receives each source's published snapshots when they arrive, see eventual consistency. Atomic activation of a combined publication in delivery sinks is a planned follow-up.