Fix redis race condition
This commit is contained in:
@@ -126,22 +126,19 @@ export const app = new Elysia()
|
||||
.group("/sync", (app) =>
|
||||
app
|
||||
// Sync status
|
||||
.get("/", async ({ }) => await productSyncer.getState())
|
||||
.get("/", async ({ }) => ({
|
||||
state: await productSyncer.getState(),
|
||||
isRunning: await productSyncer.isRunning()
|
||||
}))
|
||||
// Full sync
|
||||
.post("/", async ({ }) => {
|
||||
const syncState = await productSyncer.getState();
|
||||
if (syncState.isSyncing)
|
||||
if (!await productSyncer.sync())
|
||||
throw status(409, "Sync already in progress");
|
||||
|
||||
await productSyncer.sync();
|
||||
})
|
||||
// Per-product sync
|
||||
.post("/:printfulProductId", async ({ params }) => {
|
||||
const syncState = await productSyncer.getState();
|
||||
if (syncState.isSyncing)
|
||||
if (!await productSyncer.sync(params.printfulProductId))
|
||||
throw status(409, "Sync already in progress");
|
||||
|
||||
await productSyncer.sync(params.printfulProductId);
|
||||
}, { params: z.object({ printfulProductId: z.coerce.number() }) }),
|
||||
)
|
||||
.get("/:printfulProductId", async ({ params }) => {
|
||||
|
||||
@@ -6,21 +6,22 @@
|
||||
const { data } = $props();
|
||||
|
||||
const synchronizer = $state({
|
||||
isSyncing: false,
|
||||
syncingIds: [] as number[],
|
||||
isRunning: false,
|
||||
printfulProductIds: [] as number[],
|
||||
poll: async function () {
|
||||
const res = (await (
|
||||
await fetch(`${PUBLIC_API_URL}/products/sync`)
|
||||
).json()) as any;
|
||||
if (res.data) {
|
||||
synchronizer.isSyncing = res.data.isSyncing;
|
||||
synchronizer.syncingIds = res.data.syncingIds;
|
||||
synchronizer.isRunning = res.data.isRunning;
|
||||
synchronizer.printfulProductIds =
|
||||
res.data.state.printfulProductIds;
|
||||
}
|
||||
},
|
||||
startPolling: () => {
|
||||
const intervalId = setInterval(async () => {
|
||||
await synchronizer.poll();
|
||||
if (!synchronizer.isSyncing) clearInterval(intervalId);
|
||||
if (!synchronizer.isRunning) clearInterval(intervalId);
|
||||
}, 100);
|
||||
},
|
||||
syncAll: async () => {
|
||||
@@ -40,7 +41,7 @@
|
||||
|
||||
onMount(async () => {
|
||||
await synchronizer.poll();
|
||||
if (synchronizer.isSyncing) {
|
||||
if (synchronizer.isRunning) {
|
||||
synchronizer.startPolling();
|
||||
}
|
||||
});
|
||||
@@ -59,11 +60,11 @@
|
||||
<p>Loading...</p>
|
||||
{:then printfulProducts}
|
||||
<button
|
||||
disabled={synchronizer.isSyncing}
|
||||
disabled={synchronizer.isRunning}
|
||||
onclick={() => synchronizer.syncAll()}
|
||||
class="btn btn-xl mb-5"
|
||||
>
|
||||
{#if synchronizer.isSyncing}
|
||||
{#if synchronizer.isRunning}
|
||||
Syncing...
|
||||
{:else}
|
||||
Sync all
|
||||
@@ -98,7 +99,7 @@
|
||||
>
|
||||
<td>
|
||||
{#await isProductSynced(printfulProduct) then isSynced}
|
||||
{#if synchronizer.syncingIds.includes(printfulProduct.id)}
|
||||
{#if synchronizer.printfulProductIds.includes(printfulProduct.id)}
|
||||
<div class="badge badge-warning">
|
||||
Syncing...
|
||||
</div>
|
||||
@@ -115,7 +116,7 @@
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
disabled={synchronizer.isSyncing}
|
||||
disabled={synchronizer.isRunning}
|
||||
onclick={() =>
|
||||
synchronizer.sync(printfulProduct.id)}
|
||||
class="btn"
|
||||
|
||||
@@ -7,7 +7,7 @@ import pino from "pino";
|
||||
|
||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
type MainProduct = {
|
||||
type MetaPrintfulProduct = {
|
||||
name: string;
|
||||
externalId: string;
|
||||
colors: Set<string>;
|
||||
@@ -18,8 +18,7 @@ type MainProduct = {
|
||||
};
|
||||
|
||||
type SyncState = {
|
||||
isSyncing: boolean;
|
||||
syncingIds: number[];
|
||||
printfulProductIds: number[];
|
||||
};
|
||||
|
||||
export class ProductSyncer {
|
||||
@@ -33,14 +32,15 @@ export class ProductSyncer {
|
||||
this.log = (log ?? pino({ level: "silent" })).child({ component: "ProductSyncer" });
|
||||
}
|
||||
|
||||
async sync(printfulProductId?: number) {
|
||||
const syncStart = Date.now();
|
||||
async sync(printfulProductId?: number): Promise<boolean> {
|
||||
const canRun = await this.run();
|
||||
if (!canRun) return false;
|
||||
|
||||
try {
|
||||
const syncStart = Date.now();
|
||||
if (printfulProductId) this.log.info({ printfulProductId }, "sync started");
|
||||
else this.log.info("sync started");
|
||||
|
||||
await this.setState({ isSyncing: true, syncingIds: [] });
|
||||
|
||||
this.log.debug("populating webflow products");
|
||||
const webflowProducts: Webflow.Products.ProductAndSkus[] = await this.webflow.Products.list({ forceAll: true });
|
||||
|
||||
@@ -59,7 +59,7 @@ export class ProductSyncer {
|
||||
}
|
||||
|
||||
this.log.info({ count: printfulProducts.length }, "generating meta printful products");
|
||||
const metaPrintfulProducts: MainProduct[] = [];
|
||||
const metaPrintfulProducts: MetaPrintfulProduct[] = [];
|
||||
for (const printfulProduct of printfulProducts) {
|
||||
const mainProductName = this.getMainProductName(
|
||||
printfulProduct.name,
|
||||
@@ -93,8 +93,7 @@ export class ProductSyncer {
|
||||
for (const metaPrintfulProduct of metaPrintfulProducts) {
|
||||
this.log.info({ name: metaPrintfulProduct.name }, "syncing meta printful product");
|
||||
await this.setState({
|
||||
isSyncing: true,
|
||||
syncingIds: metaPrintfulProduct.variants.map(
|
||||
printfulProductIds: metaPrintfulProduct.variants.map(
|
||||
(v) => v.product.sync_product.id,
|
||||
),
|
||||
});
|
||||
@@ -291,22 +290,13 @@ export class ProductSyncer {
|
||||
this.log.info({ durationMs: Date.now() - syncStart }, "sync complete");
|
||||
} finally {
|
||||
try {
|
||||
await this.setState({ isSyncing: false, syncingIds: [] });
|
||||
await redis.del("commerce:sync:lock");
|
||||
await redis.del("commerce:sync:state");
|
||||
} catch {
|
||||
this.log.error("failed to reset sync state");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async setState(state: SyncState) {
|
||||
await redis.set("commerce:sync:state", JSON.stringify(state));
|
||||
await redis.expire("commerce:sync:state", 1800);
|
||||
}
|
||||
|
||||
async getState(): Promise<SyncState> {
|
||||
const val = await redis.get("commerce:sync:state");
|
||||
if (val) return JSON.parse(val);
|
||||
return { isSyncing: false, syncingIds: [] };
|
||||
return true;
|
||||
}
|
||||
|
||||
findColorInProductName(productName: string): string {
|
||||
@@ -322,4 +312,22 @@ export class ProductSyncer {
|
||||
return productName;
|
||||
}
|
||||
}
|
||||
|
||||
async isRunning(): Promise<boolean> {
|
||||
return !!await redis.get("commerce:sync:lock");
|
||||
}
|
||||
|
||||
async getState(): Promise<SyncState> {
|
||||
const val = await redis.get("commerce:sync:state");
|
||||
if (val) return JSON.parse(val);
|
||||
return { printfulProductIds: [] };
|
||||
}
|
||||
|
||||
private async setState(state: SyncState) {
|
||||
await redis.set("commerce:sync:state", JSON.stringify(state), "EX", 1800);
|
||||
}
|
||||
|
||||
private async run(): Promise<boolean> {
|
||||
return !!await redis.set("commerce:sync:lock", "1", "NX", "EX", "1800");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user