Fix redis race condition

This commit is contained in:
Dominic Ferrando
2026-06-24 23:22:02 -04:00
parent 968afb5789
commit 51abbbc00c
3 changed files with 47 additions and 41 deletions
+30 -22
View File
@@ -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");
}
}