Files
coder/coderd/database/queries/usageevents.sql
T
Dean Sheather a25d85631b chore: add usage tracking package (#19095)
Not used in coderd yet, see stack.

Adds two new packages:
- `coderd/usage`: provides an interface for the "Collector" as well as a stub implementation for AGPL
- `enterprise/coderd/usage`: provides an interface for the "Publisher" as well as a Tallyman implementation

Relates to https://github.com/coder/internal/issues/814
2025-08-16 01:31:00 +10:00

87 lines
3.4 KiB
SQL

-- name: InsertUsageEvent :exec
-- Duplicate events are ignored intentionally to allow for multiple replicas to
-- publish heartbeat events.
INSERT INTO
usage_events (
id,
event_type,
event_data,
created_at,
publish_started_at,
published_at,
failure_message
)
VALUES
(@id, @event_type, @event_data, @created_at, NULL, NULL, NULL)
ON CONFLICT (id) DO NOTHING;
-- name: SelectUsageEventsForPublishing :many
WITH usage_events AS (
UPDATE
usage_events
SET
publish_started_at = @now::timestamptz
WHERE
id IN (
SELECT
potential_event.id
FROM
usage_events potential_event
WHERE
-- Do not publish events that have already been published or
-- have permanently failed to publish.
potential_event.published_at IS NULL
-- Do not publish events that are already being published by
-- another replica.
AND (
potential_event.publish_started_at IS NULL
-- If the event has publish_started_at set, it must be older
-- than an hour ago. This is so we can retry publishing
-- events where the replica exited or couldn't update the
-- row.
-- The parenthesis around @now::timestamptz are necessary to
-- avoid sqlc from generating an extra argument.
OR potential_event.publish_started_at < (@now::timestamptz) - INTERVAL '1 hour'
)
-- Do not publish events older than 30 days. Tallyman will
-- always permanently reject these events anyways. This is to
-- avoid duplicate events being billed to customers, as
-- Metronome will only deduplicate events within 34 days.
-- Also, the same parenthesis thing here as above.
AND potential_event.created_at > (@now::timestamptz) - INTERVAL '30 days'
ORDER BY potential_event.created_at ASC
FOR UPDATE SKIP LOCKED
LIMIT 100
)
RETURNING *
)
SELECT *
-- Note that this selects from the CTE, not the original table. The CTE is named
-- the same as the original table to trick sqlc into reusing the existing struct
-- for the table.
FROM usage_events
-- The CTE and the reorder is required because UPDATE doesn't guarantee order.
ORDER BY created_at ASC;
-- name: UpdateUsageEventsPostPublish :exec
UPDATE
usage_events
SET
publish_started_at = NULL,
published_at = CASE WHEN input.set_published_at THEN @now::timestamptz ELSE NULL END,
failure_message = NULLIF(input.failure_message, '')
FROM (
SELECT
UNNEST(@ids::text[]) AS id,
UNNEST(@failure_messages::text[]) AS failure_message,
UNNEST(@set_published_ats::boolean[]) AS set_published_at
) input
WHERE
input.id = usage_events.id
-- If the number of ids, failure messages, and set published ats are not the
-- same, do not do anything. Unfortunately you can't really throw from a
-- query without writing a function or doing some jank like dividing by
-- zero, so this is the best we can do.
AND cardinality(@ids::text[]) = cardinality(@failure_messages::text[])
AND cardinality(@ids::text[]) = cardinality(@set_published_ats::boolean[]);