Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions packages/sync/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,15 @@ function buildSchedulers(
{
sweep: async (before) => {
const enqueued = await reconcileStaleCalendars(
{ resources, jobs },
{
resources,
jobs,
onEnqueueError: (error, resourceId) =>
logger.error(
`Sync reconcile sweep could not enqueue resource ${resourceId}; skipping it and continuing`,
error,
),
},
before,
() => new Date(),
);
Expand All @@ -431,7 +439,15 @@ function buildSchedulers(
{
sweep: (before) =>
maintainExpiringSubscriptions(
{ resources, jobs },
{
resources,
jobs,
onEnqueueError: (error, resourceId) =>
logger.error(
`Sync subscription sweep could not enqueue resource ${resourceId}; skipping it and continuing`,
error,
),
},
before,
() => new Date(),
),
Expand Down Expand Up @@ -497,6 +513,7 @@ function buildSchedulers(
events: repos.events,
calendars: repos.calendars,
occurrences: repos.eventOccurrences,
resources: repos.syncResources,
markers: repos.deletionMarkers,
execution: config.EXECUTION,
provider: {
Expand Down
16 changes: 16 additions & 0 deletions packages/sync/src/domain/cloud-command.service.db.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { DeletionMarkerRepository } from "@sync/storage/repositories/deletion-ma
import { EventRepository } from "@sync/storage/repositories/event.repository";
import { EventOccurrenceRepository } from "@sync/storage/repositories/event-occurrence.repository";
import { ProviderCalendarRepository } from "@sync/storage/repositories/provider-calendar.repository";
import { SyncResourceRepository } from "@sync/storage/repositories/sync-resource.repository";
import { type SyncMongoService } from "@sync/storage/sync-mongo.service";
import { beforeEach, describe, expect, it, spyOn } from "bun:test";

Expand Down Expand Up @@ -120,6 +121,7 @@ describe("submitCloudCommand provider dispatch", () => {
let commands: CommandRepository;
let events: EventRepository;
let occurrences: EventOccurrenceRepository;
let resources: SyncResourceRepository;
let calendars: ProviderCalendarRepository;
let markers: DeletionMarkerRepository;

Expand Down Expand Up @@ -181,6 +183,7 @@ describe("submitCloudCommand provider dispatch", () => {
commands = new CommandRepository(mongo.db);
events = new EventRepository(mongo.db);
occurrences = new EventOccurrenceRepository(mongo.db, mongo.client);
resources = new SyncResourceRepository(mongo.db);
calendars = new ProviderCalendarRepository(mongo.db);
markers = new DeletionMarkerRepository(mongo.db);
});
Expand All @@ -197,6 +200,7 @@ describe("submitCloudCommand provider dispatch", () => {
events,
calendars,
occurrences,
resources,
markers,
execution: "active",
provider: provider(writer),
Expand Down Expand Up @@ -224,6 +228,7 @@ describe("submitCloudCommand provider dispatch", () => {
events,
calendars,
occurrences,
resources,
markers,
execution: "passive",
provider: provider(writer),
Expand All @@ -247,6 +252,7 @@ describe("submitCloudCommand provider dispatch", () => {
events,
calendars,
occurrences,
resources,
markers,
execution: "active",
},
Expand All @@ -268,6 +274,7 @@ describe("submitCloudCommand provider dispatch", () => {
events,
calendars,
occurrences,
resources,
markers,
execution: "active",
provider: provider(writer),
Expand Down Expand Up @@ -306,6 +313,7 @@ describe("submitCloudCommand provider dispatch", () => {
events,
calendars,
occurrences,
resources,
markers,
execution: "active",
},
Expand Down Expand Up @@ -335,6 +343,7 @@ describe("submitCloudCommand provider dispatch", () => {
events,
calendars,
occurrences,
resources,
markers,
execution: "active",
},
Expand Down Expand Up @@ -415,6 +424,7 @@ describe("submitCloudCommand provider dispatch", () => {
events,
calendars,
occurrences,
resources,
markers,
execution: "passive" as const,
});
Expand Down Expand Up @@ -464,6 +474,7 @@ describe("submitCloudCommand provider dispatch", () => {
events,
calendars,
occurrences,
resources,
markers,
execution: "active",
provider: provider(writer),
Expand Down Expand Up @@ -522,6 +533,7 @@ describe("submitCloudCommand provider dispatch", () => {
events,
calendars,
occurrences,
resources,
markers,
execution: "active",
provider: provider(writer),
Expand Down Expand Up @@ -564,6 +576,7 @@ describe("submitCloudCommand provider dispatch", () => {
events,
calendars,
occurrences,
resources,
markers,
execution: "active",
provider: provider(writer),
Expand Down Expand Up @@ -598,6 +611,7 @@ describe("submitCloudCommand provider dispatch", () => {
events,
calendars,
occurrences,
resources,
markers,
execution: "active",
provider: provider(writer),
Expand Down Expand Up @@ -687,6 +701,7 @@ describe("submitCloudCommand provider dispatch", () => {
events,
calendars,
occurrences,
resources,
markers,
execution: "active",
provider: provider(writer),
Expand Down Expand Up @@ -744,6 +759,7 @@ describe("submitCloudCommand provider dispatch", () => {
events,
calendars,
occurrences,
resources,
markers,
execution: "active",
provider: provider(writer),
Expand Down
12 changes: 12 additions & 0 deletions packages/sync/src/domain/cloud-command.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { type DeletionMarkerRepository } from "@sync/storage/repositories/deleti
import { type EventRepository } from "@sync/storage/repositories/event.repository";
import { type EventOccurrenceRepository } from "@sync/storage/repositories/event-occurrence.repository";
import { type ProviderCalendarRepository } from "@sync/storage/repositories/provider-calendar.repository";
import { type SyncResourceRepository } from "@sync/storage/repositories/sync-resource.repository";

// A provider-targeted write arrived while provider work is unavailable
// (execution is passive, or no provider is configured). Nothing re-dispatches a
Expand All @@ -67,6 +68,9 @@ export interface CloudCommandDeps {
// The derived occurrence projection, rebuilt for an event's horizon whenever
// a cloud command changes it so range queries stay current.
occurrences: EventOccurrenceRepository;
// Which generation reads serve per calendar, so a provider-linked create
// projects where reads will look for it.
resources: SyncResourceRepository;
// The deletion-marker store, for the tombstone a provider delete leaves.
markers: DeletionMarkerRepository;
execution: SyncExecutionMode;
Expand Down Expand Up @@ -146,6 +150,7 @@ export async function submitCloudCommand(
commands: deps.commands,
events: deps.events,
occurrences: deps.occurrences,
resources: deps.resources,
writer: deps.provider.writer,
custody: deps.provider.custody,
},
Expand Down Expand Up @@ -339,6 +344,7 @@ async function applyCloudMutation(
commands: deps.commands,
events: deps.events,
occurrences: deps.occurrences,
resources: deps.resources,
writer: deps.provider.writer,
custody: deps.provider.custody,
},
Expand Down Expand Up @@ -386,6 +392,7 @@ async function dispatchProviderDelete(
commands: deps.commands,
events: deps.events,
occurrences: deps.occurrences,
resources: deps.resources,
writer: deps.provider.writer,
custody: deps.provider.custody,
markers: deps.markers,
Expand Down Expand Up @@ -421,6 +428,7 @@ async function dispatchProviderSeriesUpdate(
commands: deps.commands,
events: deps.events,
occurrences: deps.occurrences,
resources: deps.resources,
writer: deps.provider.writer,
custody: deps.provider.custody,
},
Expand Down Expand Up @@ -458,6 +466,7 @@ async function dispatchProviderOccurrenceDelete(
commands: deps.commands,
events: deps.events,
occurrences: deps.occurrences,
resources: deps.resources,
writer: deps.provider.writer,
custody: deps.provider.custody,
},
Expand Down Expand Up @@ -488,6 +497,7 @@ async function dispatchProviderOccurrenceUpdate(
commands: deps.commands,
events: deps.events,
occurrences: deps.occurrences,
resources: deps.resources,
writer: deps.provider.writer,
custody: deps.provider.custody,
},
Expand Down Expand Up @@ -518,6 +528,7 @@ async function dispatchProviderSeriesFollowingDelete(
commands: deps.commands,
events: deps.events,
occurrences: deps.occurrences,
resources: deps.resources,
writer: deps.provider.writer,
custody: deps.provider.custody,
markers: deps.markers,
Expand Down Expand Up @@ -549,6 +560,7 @@ async function dispatchProviderSeriesFollowingUpdate(
commands: deps.commands,
events: deps.events,
occurrences: deps.occurrences,
resources: deps.resources,
writer: deps.provider.writer,
custody: deps.provider.custody,
},
Expand Down
26 changes: 26 additions & 0 deletions packages/sync/src/domain/failed-job-requeue.service.db.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,30 @@ describe("requeueFailedJobs", () => {
exhaustedJobs: [],
});
});

it("requeues a job written before requeuedCount existed", async () => {
// Mongo's {$lt: n} does not match a missing field, so the self-heal sweep
// could not see the very jobs most likely to be wedged: the ones old
// enough to predate its own bookkeeping field. Three such jobs sat failed
// in prod while this sweep reported nothing to do (2026-07-31).
const id = await seedFailed({
runAfter: new Date("2026-07-20T10:00:00.000Z"),
});
await storage
.db()
.collection("jobs")
.updateOne({ _id: id as never }, { $unset: { requeuedCount: "" } });

const result = await requeueFailedJobs(deps(), cooldownBefore, now, 3);

expect(result.requeued).toBe(1);
expect(result.exhausted).toBe(0);
const raw = await storage
.db()
.collection("jobs")
.findOne({ _id: id as never });
expect(raw?.state).toBe("pending");
// Absence counted as zero, so the requeue is its first, not its last.
expect(raw?.requeuedCount).toBe(1);
});
});
Loading