Extract product sync queue db calls into Queue Service methods
This commit is contained in:
@@ -5,9 +5,9 @@ import { DatabaseError } from "pg";
|
|||||||
import type { Insertable, Selectable } from "kysely";
|
import type { Insertable, Selectable } from "kysely";
|
||||||
import type { ProductSyncs } from "../database/out/db";
|
import type { ProductSyncs } from "../database/out/db";
|
||||||
|
|
||||||
type SyncStatus = "queued" | "active" | "failed" | "succeeded";
|
type SyncStatus = "queued" | "active" | "failed" | "fulfilled";
|
||||||
|
|
||||||
type QueuedProductSync = {
|
type ProductSyncPayload = {
|
||||||
id?: string,
|
id?: string,
|
||||||
session: {
|
session: {
|
||||||
name: string,
|
name: string,
|
||||||
@@ -40,38 +40,15 @@ class ProductSyncService {
|
|||||||
this.ProductSyncer = new ProductSyncer(this.Printful, this.Webflow, log);
|
this.ProductSyncer = new ProductSyncer(this.Printful, this.Webflow, log);
|
||||||
}
|
}
|
||||||
|
|
||||||
async start(queuedProductSync: QueuedProductSync) {
|
async start(payload: ProductSyncPayload) {
|
||||||
let id = queuedProductSync.id ?? null;
|
let id = payload.id ?? null;
|
||||||
let freedSlot = false;
|
let freedSlot = false;
|
||||||
try {
|
try {
|
||||||
log.info({ session: queuedProductSync.session }, "starting sync run");
|
log.info({ session: payload.session }, "starting sync run");
|
||||||
await this.ProductSyncer.run({
|
await this.ProductSyncer.run({
|
||||||
filter: queuedProductSync.filter,
|
filter: payload.filter,
|
||||||
onStart: async (startDate) => {
|
onStart: async (startDate) => {
|
||||||
const values: Insertable<ProductSyncs> = {
|
await this.Queue.enqueue(payload, startDate);
|
||||||
session_id: queuedProductSync.session.id,
|
|
||||||
session_name: queuedProductSync.session.name,
|
|
||||||
started_at: startDate,
|
|
||||||
p_product_id_filter: queuedProductSync.filter?.pProductIds ?? null,
|
|
||||||
};
|
|
||||||
if (id) {
|
|
||||||
const result = await db.updateTable("product_syncs")
|
|
||||||
.set(values)
|
|
||||||
.where("id", "=", id)
|
|
||||||
.where("started_at", "is", null)
|
|
||||||
.executeTakeFirst();
|
|
||||||
|
|
||||||
// Guard against another worker/machine having already claimed
|
|
||||||
// this queued row between `queue.front()` and now.
|
|
||||||
if (result.numUpdatedRows === 0n) throw new AlreadyClaimedError();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const result = await db.insertInto("product_syncs")
|
|
||||||
.values(values)
|
|
||||||
.returning("id")
|
|
||||||
.executeTakeFirstOrThrow();
|
|
||||||
id = result.id;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onStep: async (pProductIds: number[]) => {
|
onStep: async (pProductIds: number[]) => {
|
||||||
if (!id) throw new Error("Expected sync run ID");
|
if (!id) throw new Error("Expected sync run ID");
|
||||||
@@ -82,10 +59,7 @@ class ProductSyncService {
|
|||||||
},
|
},
|
||||||
onCompletion: async (completionDate, duration) => {
|
onCompletion: async (completionDate, duration) => {
|
||||||
if (!id) throw new Error("Expected sync run ID");
|
if (!id) throw new Error("Expected sync run ID");
|
||||||
await db.updateTable("product_syncs")
|
await this.Queue.fulfill(id, completionDate)
|
||||||
.set({ syncing_p_product_ids: [], ended_at: completionDate })
|
|
||||||
.where("id", "=", id)
|
|
||||||
.execute();
|
|
||||||
freedSlot = true;
|
freedSlot = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -103,23 +77,17 @@ class ProductSyncService {
|
|||||||
// If active sync has been syncing for 10 min (600000ms), clear it and retry this sync
|
// If active sync has been syncing for 10 min (600000ms), clear it and retry this sync
|
||||||
if (Date.now() - (activeSync.started_at?.getTime() ?? 0) > 600000) {
|
if (Date.now() - (activeSync.started_at?.getTime() ?? 0) > 600000) {
|
||||||
log.info("already active sync exceeded timeout");
|
log.info("already active sync exceeded timeout");
|
||||||
await db.updateTable("product_syncs")
|
await this.Queue.fail(activeSync.id, new Date());
|
||||||
.set({ syncing_p_product_ids: [], ended_at: new Date(), has_failed: true })
|
await this.start(payload);
|
||||||
.where("id", "=", activeSync.id)
|
|
||||||
.execute();
|
|
||||||
await this.start(queuedProductSync);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
log.info("sync already active, enqueuing next sync");
|
log.info("sync already active, enqueuing next sync");
|
||||||
await this.Queue.enqueue(queuedProductSync);
|
await this.Queue.enqueue(payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (id) {
|
if (id) {
|
||||||
await db.updateTable("product_syncs")
|
await this.Queue.fail(id, new Date());
|
||||||
.set({ syncing_p_product_ids: [], ended_at: new Date(), has_failed: true })
|
|
||||||
.where("id", "=", id)
|
|
||||||
.execute();
|
|
||||||
freedSlot = true;
|
freedSlot = true;
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
@@ -164,31 +132,39 @@ class ProductSyncService {
|
|||||||
deriveStatus(sync: Selectable<ProductSyncs>): SyncStatus {
|
deriveStatus(sync: Selectable<ProductSyncs>): SyncStatus {
|
||||||
if (!sync.started_at) return "queued";
|
if (!sync.started_at) return "queued";
|
||||||
if (!sync.ended_at) return "active";
|
if (!sync.ended_at) return "active";
|
||||||
return sync.has_failed ? "failed" : "succeeded";
|
return sync.has_failed ? "failed" : "fulfilled";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProductSyncQueueService {
|
class ProductSyncQueueService {
|
||||||
async enqueue(queuedProductSync: QueuedProductSync) {
|
async enqueue(payload: ProductSyncPayload, startDate?: Date): Promise<string> {
|
||||||
const values: Insertable<ProductSyncs> = {
|
const values: Insertable<ProductSyncs> = {
|
||||||
session_id: queuedProductSync.session.id,
|
session_id: payload.session.id,
|
||||||
session_name: queuedProductSync.session.name,
|
session_name: payload.session.name,
|
||||||
p_product_id_filter: queuedProductSync.filter?.pProductIds ?? null
|
p_product_id_filter: payload.filter?.pProductIds ?? null
|
||||||
};
|
};
|
||||||
if (queuedProductSync.id) {
|
if (startDate) values.started_at = startDate;
|
||||||
await db.updateTable("product_syncs")
|
if (payload.id) {
|
||||||
|
const result = await db.updateTable("product_syncs")
|
||||||
.set(values)
|
.set(values)
|
||||||
.where("id", "=", queuedProductSync.id)
|
.where("id", "=", payload.id)
|
||||||
.execute();
|
.where("started_at", "is", null)
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
// Guard against another worker/machine having already claimed
|
||||||
|
// this queued row between `queue.front()` and now.
|
||||||
|
if (result.numUpdatedRows === 0n) throw new AlreadyClaimedError();
|
||||||
|
return payload.id;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
await db.insertInto("product_syncs")
|
const result = await db.insertInto("product_syncs")
|
||||||
.values(values)
|
.values(values)
|
||||||
.execute();
|
.returning("id")
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
return result.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async front(): Promise<QueuedProductSync | undefined> {
|
async front(): Promise<ProductSyncPayload | undefined> {
|
||||||
const result = await db.selectFrom("product_syncs")
|
const result = await db.selectFrom("product_syncs")
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.where("started_at", "is", null)
|
.where("started_at", "is", null)
|
||||||
@@ -208,4 +184,18 @@ class ProductSyncQueueService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fail(id: string, endDate: Date) {
|
||||||
|
await db.updateTable("product_syncs")
|
||||||
|
.set({ syncing_p_product_ids: [], ended_at: endDate, has_failed: true })
|
||||||
|
.where("id", "=", id)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fulfill(id: string, endDate: Date) {
|
||||||
|
await db.updateTable("product_syncs")
|
||||||
|
.set({ syncing_p_product_ids: [], ended_at: endDate })
|
||||||
|
.where("id", "=", id)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { api } from "$lib/api.js";
|
import { api } from "$lib/api.js";
|
||||||
|
|
||||||
type SyncStatus = "queued" | "active" | "failed" | "succeeded" | "none";
|
type SyncStatus = "queued" | "active" | "failed" | "fulfilled" | "none";
|
||||||
|
|
||||||
const { data } = $props();
|
const { data } = $props();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user