Ctrl K
ComunidadGuías…

scripts/replay-posthog-dlq.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import type { QueueApi, QueueEnvironment } from './lib/posthog-queues.ts'
import { Buffer } from 'node:buffer'
import process from 'node:process'
import { replayablePostHogMessage } from '../supabase/functions/_backend/utils/posthog_queue.ts'
import { createQueueApi, queueEnvironment, verifiedQueue } from './lib/posthog-queues.ts'

interface PullMessage {
  body: string
  attempts: number
  lease_id?: string
  metadata?: { 'CF-Content-Type'?: string }
}

function decodeMessage(entry: PullMessage) {
  try {
    if (entry.metadata?.['CF-Content-Type'] !== 'json' || typeof entry.body !== 'string' || entry.body.length > 180 * 1024
      || !/^(?:[A-Z0-9+/]{4})*(?:[A-Z0-9+/]{2}==|[A-Z0-9+/]{3}=)?$/i.test(entry.body)) {
      return undefined
    }
    return replayablePostHogMessage(JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.from(entry.body, 'base64'))))
  }
  catch {
    return undefined
  }
}

export async function replayPostHogDlq(api: QueueApi, environment: QueueEnvironment, execute = false, limit = 10) {
  if (!Number.isInteger(limit) || limit < 1 || limit > 100)
    throw new Error('Replay limit must be an integer from 1 to 100')
  const main = await verifiedQueue(api, `capgo-posthog-events-${environment}`)
  const dlq = await verifiedQueue(api, `capgo-posthog-events-${environment}-dlq`)
  if (!execute) {
    // Dry run never acquires a lease (pulling changes queue state and attempts).
    const metrics = await api<{ backlog_count: number }>(`/${dlq.queue_id}/metrics`)
    if (typeof metrics.backlog_count !== 'number' || !Number.isFinite(metrics.backlog_count) || metrics.backlog_count < 0)
      throw new Error('Invalid backlog metrics')
    console.log(JSON.stringify({ mode: 'dry_run', backlog_count: metrics.backlog_count, limit }))
    return { published: 0, refused: 0 }
  }
  if (dlq.consumers?.length !== 1 || dlq.consumers[0].type !== 'http_pull')
    throw new Error('DLQ requires the provisioned HTTP pull consumer')
  let published = 0
  let refused = 0
  const deadline = Date.now() + 8 * 60 * 1000
  // At most ten leased messages at once; never pull more than this run's bound.
  while (published + refused < limit && Date.now() < deadline) {
    // Peek does not consume retry budget. Refuse known-invalid or near-exhaustion
    // messages before leasing; run only one operator replay process per environment.
    const size = Math.min(10, limit - published - refused)
    const preview = await api<{ messages: PullMessage[] }>(`/${dlq.queue_id}/messages/peek`, 'POST', { batch_size: size })
    if (!Array.isArray(preview.messages) || preview.messages.length > size)
      throw new Error('Invalid preview response')
    refused = preview.messages.filter(entry => !decodeMessage(entry) || !Number.isInteger(entry.attempts) || entry.attempts < 0 || entry.attempts >= 4).length
    if (refused || !preview.messages.length)
      break
    const leaseStarted = Date.now()
    const result = await api<{ messages: PullMessage[] }>(`/${dlq.queue_id}/messages/pull`, 'POST', {
      batch_size: size,
      visibility_timeout_ms: 600000,
    })
    if (!Array.isArray(result.messages) || result.messages.length > size)
      throw new Error('Invalid pull response')
    if (!result.messages.length)
      break
    for (const entry of result.messages) {
      if (Date.now() >= deadline || Date.now() - leaseStarted > 9 * 60 * 1000)
        throw new Error('Replay deadline reached; remaining leases are unacknowledged')
      const message = typeof entry.lease_id === 'string' && entry.lease_id ? decodeMessage(entry) : undefined
      if (!message) {
        refused++
        continue
      }
      await api(`/${main.queue_id}/messages`, 'POST', { body: message, content_type: 'json' })
      // A crash after publish and before ack duplicates delivery; canonical IDs dedupe it.
      const ack = await api<{ ackCount: number, warnings?: Record<string, unknown> }>(`/${dlq.queue_id}/messages/ack`, 'POST', {
        acks: [{ lease_id: entry.lease_id }],
        retries: [],
      })
      if (ack.ackCount !== 1 || (ack.warnings && Object.keys(ack.warnings).length))
        throw new Error('DLQ acknowledgement was not confirmed')
      published++
    }
    // Stop after invalid data rather than repeatedly leasing it until retries exhaust.
    if (refused)
      break
  }
  console.log(JSON.stringify({ mode: 'execute', published, refused }))
  return { published, refused }
}

if (import.meta.main) {
  try {
    const args = process.argv.slice(2)
    const environment = queueEnvironment(args[0] ?? '')
    if (args.slice(1).some(arg => arg !== '--execute' && !/^--limit=\d+$/.test(arg)))
      throw new Error('Usage: bun scripts/replay-posthog-dlq.ts <alpha|preprod|prod> [--execute] [--limit=10]')
    const limitArg = args.find(arg => arg.startsWith('--limit='))
    const result = await replayPostHogDlq(createQueueApi(), environment, args.includes('--execute'), limitArg ? Number(limitArg.slice(8)) : 10)
    if (result.refused)
      process.exitCode = 1
  }
  catch (error) {
    console.error(error instanceof Error ? error.message : 'Replay failed; no payload was logged')
    process.exitCode = 1
  }
}