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