feat(render+identity): daily render-limit — consume on submit, refund on admin-stop
Build backend images / build content-svc (push) Failing after 51s
Build backend images / build file-svc (push) Failing after 53s
Build backend images / build gateway (push) Failing after 1m1s
Build backend images / build identity-svc (push) Failing after 48s
Build backend images / build notification-svc (push) Failing after 42s
Build backend images / build render-svc (push) Failing after 47s
Build backend images / build studio-svc (push) Failing after 1m13s

Business rule: each user has a daily render limit. Admin-stop refunds the used
charge (not the user's fault); a user's own cancel does not.

- identity: ConsumeRenderChargeAsync / RefundRenderChargeAsync on DailyRemainRenderCount
  with lazy daily reset (mig 24: daily_renders_reset_at). Convention: max=0 ⇒ UNLIMITED,
  so existing 0/0 users keep rendering until an admin sets a real limit.
- identity InternalController (service-token): POST /v1/internal/render-charge/{consume,refund}
- render-svc: identityclient + on Create consume (block 429 when limit reached, fail-open
  on identity outage); on admin Stop refund the job owner; user /cancel unchanged
- compose: IDENTITY_URL for render-svc, ServiceToken for identity-svc

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-03 02:18:00 +03:30
parent 7f7feabb85
commit 1f52f53cf7
9 changed files with 215 additions and 13 deletions
+12 -8
View File
@@ -597,22 +597,26 @@ func (s *Store) CancelJob(ctx context.Context, id, userID uuid.UUID) (bool, erro
return tag.RowsAffected() > 0, err
}
// StopJob cancels any in-progress job regardless of owner (admin action). Also
// frees the assigned node so it can pick up new work.
func (s *Store) StopJob(ctx context.Context, id uuid.UUID) (bool, error) {
tag, err := s.pool.Exec(ctx, `
// StopJob cancels any in-progress job regardless of owner (admin action), frees the
// assigned node, and returns the job's owner id so the caller can refund their charge.
func (s *Store) StopJob(ctx context.Context, id uuid.UUID) (bool, uuid.UUID, error) {
var ownerID uuid.UUID
err := s.pool.QueryRow(ctx, `
UPDATE render.render_jobs
SET step = 'Cancelled'::render_step, completed_at = NOW(), updated_at = NOW()
WHERE id = $1 AND step NOT IN ('Done','Failed','Cancelled')`,
id)
WHERE id = $1 AND step NOT IN ('Done','Failed','Cancelled')
RETURNING user_id`, id).Scan(&ownerID)
if err == pgx.ErrNoRows {
return false, uuid.Nil, nil
}
if err != nil {
return false, err
return false, uuid.Nil, err
}
// Release any node that was actively working this job.
_, _ = s.pool.Exec(ctx,
`UPDATE render.render_nodes SET status = 'Ready'::node_status, current_frame_job_id = NULL, updated_at = NOW()
WHERE current_frame_job_id IN (SELECT id FROM render.frame_jobs WHERE render_job_id = $1)`, id)
return tag.RowsAffected() > 0, err
return true, ownerID, nil
}
func (s *Store) GetJobProgress(ctx context.Context, id, userID uuid.UUID) (*models.RenderJob, error) {