Skip to content

feat: entitlements based on clickhouse direct - #21129

Draft
julianpoy wants to merge 2 commits into
mainfrom
ent-clickhouse
Draft

feat: entitlements based on clickhouse direct#21129
julianpoy wants to merge 2 commits into
mainfrom
ent-clickhouse

Conversation

@julianpoy

Copy link
Copy Markdown
Member

DRAFT

@julianpoy
julianpoy force-pushed the ent-clickhouse branch 3 times, most recently from d7ecf0b to 629434c Compare September 1, 2026 20:33
Comment on lines +135 to +151
recent AS
(
SELECT subject, min(event_time) AS first_event
FROM ${EVENTS_TABLE}
WHERE client_id = {clientId:String}
AND slug = {slug:String}
AND ingested_at >= {ingestedSince:DateTime64(3, 'UTC')}
AND event_time >= {eventTimeFloor:DateTime64(3, 'UTC')}
AND event_time < {to:DateTime64(3, 'UTC')}
GROUP BY subject
)
SELECT
r.subject AS subject,
formatDateTime(r.first_event, '%Y-%m-%dT%H:%i:%S.%fZ', 'UTC') AS started_at
FROM recent AS r
LEFT JOIN last_session AS l ON r.subject = l.subject
WHERE r.first_event > coalesce(l.started_at, toDateTime64(0, 3, 'UTC')) + toIntervalSecond({durationSeconds:UInt32})`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concern: Could session detection silently drop events?
recent aggregates to a single min(event_time) per subject before the gap check.

If one sweep's ingest window contains both an in-session event and a post-expiry one, min would pick the in-session event, fail the gap test, and open no session for either.

Suggestion:
Filter before aggregating rather than after

const FIND_NEW_SESSION_STARTS_SQL = `
WITH
last_session AS
(
    SELECT subject, max(session_start) AS started_at
    FROM ${SESSIONS_TABLE}
    WHERE client_id = {clientId:String}
      AND slug = {slug:String}
    GROUP BY subject
)
SELECT
    e.subject AS subject,
    formatDateTime(
        min(e.event_time), '%Y-%m-%dT%H:%i:%S.%fZ', 'UTC'
    ) AS started_at
FROM ${EVENTS_TABLE} AS e
LEFT JOIN last_session AS l ON e.subject = l.subject
WHERE e.client_id = {clientId:String}
  AND e.slug = {slug:String}
  AND e.ingested_at >= {ingestedSince:DateTime64(3, 'UTC')}
  AND e.event_time >= {eventTimeFloor:DateTime64(3, 'UTC')}
  AND e.event_time < {to:DateTime64(3, 'UTC')}
  -- Gate every event on the session gap, then take the earliest survivor.
  -- Checking the gap against min(event_time) instead lets an in-session event
  -- shield a later post-expiry one, so no session opens for either.
  AND e.event_time > coalesce(l.started_at, toDateTime64(0, 3, 'UTC'))
                     + toIntervalSecond({durationSeconds:UInt32})
GROUP BY e.subject`;

Comment on lines +142 to +175
const watermark =
(await this.meteringSweepManager.findWatermark(params)) ??
new Date(now.getTime() - this.meteringSweepConfig.lookbackMs);
const ingestedSince = new Date(
watermark.getTime() - this.meteringSweepConfig.watermarkLagMs
);

const thresholds = meterResult
.getNotificationThresholds()
.filter((threshold) => threshold > 0);

const { failed, ...summary } = await this.notifyCrossings(
params,
meter,
thresholds,
ingestedSince,
now
);

if (failed > 0) {
this.statsd.increment('metering.sweep.watermark_held', {
slug: params.slug,
});
return { ...summary, held: true, watermark: watermark.toISOString() };
}

await this.meteringSweepManager.advanceWatermark({
...params,
watermark: now,
updatedAt: now,
});

return { ...summary, held: false, watermark: now.toISOString() };
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concern: Could a meter that outgrows the ClickHouse caps stall permanently rather than degrade?

ingestedSince is derived from the stored watermark and advanceWatermark is only reached on success. So if findWindowCandidates trips maxExecutionTimeSeconds or requestTimeoutMs, wouldn't the watermark stay frozen while now keeps advancing, making the active CTE's range larger on every retry, so the meter can't recover on its own?

): Promise<SubjectDispatch> {
const { params, meter, thresholds, lastSent, now } = context;

const grantedAmount = await this.usageGrantsManager.getActiveGrantedAmount(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the cooldown check come before this lookup?

lastSent is already in memory, and windowId doesn't depend on grantedAmount.

With cooldownMs at 1h, a recently notified subject gets re-read from Firestore on every sweep until it's eligible again (each answer is discarded). I think skipping looks safe since computeThresholdMet only filters thresholds. If none of them clears the cooldown, nothing in met will either.

this.logger.error(err);
throw err;

const response = await fetch(params.url, {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the URL need to be validated?

let failed = 0;
for (const meter of meters) {
try {
const result = await this.sweep(meter);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should now be passed into this? -> const result = await this.sweep(meter, now);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants