diff --git a/src/promotion-routes.ts b/src/promotion-routes.ts index 6123c4f..97f1f6e 100644 --- a/src/promotion-routes.ts +++ b/src/promotion-routes.ts @@ -181,7 +181,7 @@ export function mountPromotion(app: Hono, d: PromotionDeps) { app.get(`${V}/placements/:id`, api((c) => promo.placement(c.req.param('id')!))) app.post(`${V}/campaigns/:id/lease`, api(async (c) => { const b = await body(c) - return promo.lease(c.req.param('id')!, String(b.owner ?? 'worker').slice(0, 60), Math.min(20, Number(b.max ?? 5) || 5)) + return promo.lease(c.req.param('id')!, String(b.owner ?? 'worker').slice(0, 60), Math.min(50, Number(b.max ?? 20) || 20)) })) app.post(`${V}/placements/:id/report`, api(async (c) => promo.report(c.req.param('id')!, (await body(c)) as any, actor(c)))) app.post(`${V}/placements/:id/retry`, api((c) => promo.retry(c.req.param('id')!, actor(c)))) diff --git a/src/promotion-worker.ts b/src/promotion-worker.ts index c974dad..39c5673 100644 --- a/src/promotion-worker.ts +++ b/src/promotion-worker.ts @@ -121,7 +121,7 @@ export type WorkerResult = { placement: string; action: string; state?: string; /** One pass over what the lease hands out. Returns what it did, per placement. */ export async function workOnce(client: PitClient, campaignId: string, myna: Myna, options: { owner?: string; max?: number; fetch?: typeof fetch } = {}): Promise { - const { placements, reason } = await client.promo<{ placements: Placement[]; reason?: string }>('POST', `/campaigns/${encodeURIComponent(campaignId)}/lease`, { owner: options.owner ?? 'myna-worker', max: options.max ?? 5 }) + const { placements, reason } = await client.promo<{ placements: Placement[]; reason?: string }>('POST', `/campaigns/${encodeURIComponent(campaignId)}/lease`, { owner: options.owner ?? 'myna-worker', max: options.max ?? 20 }) if (reason) return [{ placement: '-', action: 'idle', detail: reason }] const out: WorkerResult[] = [] const report = async (p: Placement, body: Record, action: string) => { diff --git a/src/promotion.ts b/src/promotion.ts index 4bf0afe..a4efcaa 100644 --- a/src/promotion.ts +++ b/src/promotion.ts @@ -647,7 +647,7 @@ export class Promotion { .query(`SELECT * FROM promo_placements WHERE campaign_id = ? AND (lease_until IS NULL OR lease_until < ?) AND ((channel = 'myna' AND state IN ('ready', 'publishing') AND planned_at <= ?) OR state IN ('scheduled', 'uncertain') OR (state = 'published' AND account NOT LIKE 'linkedin:%')) - ORDER BY planned_at LIMIT ?`) + ORDER BY CASE WHEN state IN ('ready', 'publishing') THEN 0 ELSE 1 END, CASE WHEN state IN ('ready', 'publishing') THEN planned_at ELSE updated_at END LIMIT ?`) .all(c.id, now, now, max) as any[] const leased = [] for (const r of rows) { diff --git a/test/promotion.test.ts b/test/promotion.test.ts index 4b52da6..f228f85 100644 --- a/test/promotion.test.ts +++ b/test/promotion.test.ts @@ -317,6 +317,20 @@ describe('promotion: measurement', () => { expect(h.promo.placements(h.campaign, { state: 'verified' }).placements.map((p) => p.kind)).toEqual(['roundup']) }) + it('leases due posts before re-checking scheduled ones, and rotates the re-checks', async () => { + const h = setup() + await launched(h) + const all = h.promo.placements(h.campaign).placements + const [first, second] = all.filter((p) => p.kind === 'spotlight') + // The announcement sits in myna's queue for weeks: it is re-checked, but never ahead of a due post. + const announce = all.find((p) => p.kind === 'announcement')! + h.promo.lease(h.campaign, 'w', 1) + h.promo.report(announce.id, { state: 'scheduled', queueId: 'q' }) + h.clock.t = second!.plannedAt + 1 + const leased = h.promo.lease(h.campaign, 'w', 2).placements.map((p) => p.id) + expect(leased).toEqual([first!.id, second!.id]) + }) + it('records gaps and reports them', async () => { const h = setup() await launched(h)