You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
wiki/backend/tasks/simple/sync-storage-targets.ts

70 lines
3.4 KiB

/**
* Sync every storage target that has a remote to keep in step with, and whose site is due.
*
* The git target's schedule, and what makes it a synchronized store rather than a local repository
* that happens to get committed to. A change is committed the moment it is made — that happens on the
* request, not here — and this is what pushes those commits and brings back what other people pushed.
*
* **This runs every minute; the site's `syncInterval` decides what actually happens.** The interval is
* a per-site setting, so one schedule cannot express it — the tick is therefore as fine as the
* shortest interval anybody can ask for, and each site is skipped on the ticks that are not its own.
*
* Due-ness is read off the clock rather than off a record of when each target last synced. A site with
* a five-minute interval syncs on every fifth minute of the epoch, which needs nothing stored, means
* two instances agree without coordinating, survives a restart, and cannot drift. What it gives up is
* catching up on a missed tick: an instance that was down at the moment simply waits for the next one,
* which for something that runs all day is the right trade.
*
* Every target is attempted whatever the ones before it did: a site whose credentials have expired
* must not stop the rest of them syncing. `executeAction` records the outcome on the target itself,
* so a failure shows up on its Status card in the admin area rather than only in this log.
*
* **One instance at a time.** The scheduler hands a job to a single instance, which is the one whose
* working copy is synced. Every instance in a high-availability set keeps its own, so they each fall
* in step at their own turn rather than fighting over one repository.
*/
export async function task(): Promise<void> {
const syncable = await WIKI.models.storage.syncableTargets()
if (syncable.length < 1) {
return
}
const minute = Math.floor(Temporal.Now.instant().epochMilliseconds / 60_000)
const targets = syncable.filter((target) => {
const interval = WIKI.models.storage.syncIntervalFor(target.siteId)
// -> An interval nothing can be made of is a site that is never synced on a schedule, rather than
// one synced every minute. `validateSiteConfig` refuses to store such a value in the first
// place, so this is the belt to that braces.
return interval > 0 && minute % interval === 0
})
if (targets.length < 1) {
return
}
// -> A pull creates content, and content records who authored it. There is nobody behind a
// scheduled run, so it is recorded against the wiki's own administrator.
const actorId = await WIKI.models.users.getSystemActorId()
if (!actorId) {
WIKI.logger.warn(
'Syncing storage targets: no active administrator to attribute incoming content to [ SKIPPED ]'
)
return
}
WIKI.logger.info(`Syncing ${targets.length} storage target(s)...`)
let failed = 0
for (const target of targets) {
try {
const message = await WIKI.models.storage.executeAction(target, 'sync', actorId)
WIKI.logger.info(`Synced ${target.title} for site ${target.siteId}: ${message ?? 'done'}`)
} catch (err: any) {
failed++
WIKI.logger.warn(`Could not sync ${target.title} for site ${target.siteId} [ FAILED ]`)
WIKI.logger.warn(err.message)
}
}
WIKI.logger.info(
`Syncing storage targets: ${targets.length - failed} of ${targets.length} succeeded [ COMPLETED ]`
)
}