multica-ai/multica · error

scan task_usage time range: %w

Error message

scan task_usage time range: %w

What it means

Scanning SELECT MIN(created_at), MAX(created_at) FROM task_usage failed. Failure modes: the task_usage table does not exist (schema not migrated — error 42P01), a column type mismatch when scanning into pgtype.Timestamptz, the query was cancelled, or the connection dropped. Note MIN/MAX return NULL on an empty table, which pgtype.Timestamptz scans as Valid=false — that is handled, not an error.

Source

Thrown at server/cmd/backfill_task_usage_hourly/main.go:119

	// is held on a dedicated session connection for the whole run.
	lockConn, err := pool.Acquire(ctx)
	if err != nil {
		return fmt.Errorf("acquire advisory-lock connection: %w", err)
	}
	defer lockConn.Release()
	if _, err := lockConn.Exec(ctx, `SELECT pg_advisory_lock(4246)`); err != nil {
		return fmt.Errorf("acquire advisory lock 4246: %w", err)
	}
	defer func() {
		// Unlock on a fresh context so a cancelled ctx (SIGINT) does not
		// skip the release. Releasing the connection afterwards would end
		// the session anyway, but an explicit unlock frees it immediately.
		_, _ = lockConn.Exec(context.Background(), `SELECT pg_advisory_unlock(4246)`)
	}()

	var minTS, maxTS pgtype.Timestamptz
	if err := pool.QueryRow(ctx, `SELECT MIN(created_at), MAX(created_at) FROM task_usage`).Scan(&minTS, &maxTS); err != nil {
		return fmt.Errorf("scan task_usage time range: %w", err)
	}
	if !minTS.Valid {
		slog.Info("task_usage is empty; nothing to backfill")
		if *dryRun {
			return nil
		}
		return stampWatermark(ctx, pool)
	}

	from := monthFloor(minTS.Time.UTC())
	end := monthFloor(maxTS.Time.UTC()).AddDate(0, 1, 0)

	if *monthsBack > 0 {
		cutoff := monthFloor(time.Now().UTC()).AddDate(0, -(*monthsBack), 0)
		// A partial backfill still stamps the watermark at now()-5min, so
		// buckets older than the cutoff are abandoned permanently: the cron
		// worker will never look back that far. That data loss must be an
		// explicit operator decision — require --force-partial to proceed.

View on GitHub (pinned to 2c0912b6ec)

Solutions

  1. Run the migration tool first (server/cmd/migrate) so task_usage exists
  2. Confirm the DSN targets the right database (\dt in psql shows task_usage)
  3. Check created_at's column type matches timestamptz expectations
Defensive patterns

Strategy: validation

Validate before calling

var exists bool
pool.QueryRow(ctx, `SELECT to_regclass('task_usage') IS NOT NULL`).Scan(&exists)
if !exists { /* run migrations first */ }

Try / catch

if err := pool.QueryRow(ctx, `SELECT MIN(created_at), MAX(created_at) FROM task_usage`).Scan(&minTS, &maxTS); err != nil {
    return fmt.Errorf("scan task_usage time range: %w", err)
}

Prevention

When it happens

Trigger: Running the backfill against a database where migrations have not created task_usage; scanning a created_at column whose type is not timestamptz (e.g. migrated to a different type); ctx cancelled mid-query.

Common situations: Fresh environment where `migrate up` was skipped; pointing -db at the wrong database; Postgres restart mid-query.

Related errors


AI-assisted analysis of multica-ai/multica@2c0912b6ec (2026-08-15). Data as JSON: /api/errors/8fef7c65cc97806a. Report an issue: GitHub.