Files
sub2api/backend/migrations
Brisbanehuang 20ad5ec506 feat(scheduler): per-group profit control for token account admission
Group pricing (rate multiplier, peak windows, per-user overrides) and
account cost (accounts.rate_multiplier) already live side by side, but
nothing stops the scheduler from handing a request to an account whose
cost multiplier exceeds what the group's pricing can profitably serve.
Add an opt-in per-group profit gate that filters scheduling candidates
by a margin rule, while ordering, scoring, stickiness and breakers keep
working unchanged among qualified accounts.

Admission rule: an account qualifies iff U <= D * (1 - min_margin -
safety_buffer) within a small relative epsilon, where U is
accounts.rate_multiplier (0 is legal; missing/negative/NaN/Inf are
conservatively rejected as invalid) and D is the requester's effective
downstream multiplier (user-group override ?? group default, times the
group peak factor) frozen at the request's pricing instant.

- groups gain profit_control_enabled / profit_min_margin /
  profit_safety_buffer (migration 191); the durable auth-cache
  invalidation trigger additionally watches the profit and pricing
  columns (migration 192) so out-of-band group edits cannot leave
  stale auth snapshots; GetByKeyForAuth explicitly projects the new
  columns and the API-key auth snapshot version is bumped to force a
  refresh of pre-existing snapshots
- request-level pricing instant: token entry points install pricingAt
  into ctx; the profit threshold D and the RecordUsage peak factor
  read the same instant, so one request never changes price mid-flight
  across waits/retries/failover (media and unwired paths keep the
  existing record-time semantics)
- the gate covers token requests on openai, anthropic, gemini, grok
  and antigravity groups: OpenAI-family handlers via
  WithOpenAIRequestPricingContext (responses incl. WS bridge, chat
  completions, messages, embeddings, alpha search), the shared gateway
  via WithGatewayTokenRequestPricing (messages, chat completions,
  responses, gemini model actions); composite groups cannot enable it
  directly; image/video/models/usage/count_tokens stay ungated and an
  explicit image-generation intent suppresses the gate end to end
- post-slot recheck: after a slot is acquired the account is re-read
  via SchedulerSnapshotService.GetAccount (scheduler cache, then DB;
  only when both fail the check fails open with WARN + metric); a
  vetoed account releases its slot and joins the request's exclusion
  set for reselection; sticky bindings are written only after the
  final check passes, and an over-threshold sticky account is skipped,
  not deleted, so it comes back once its rate recovers
- sticky-session cache contract: GatewayCache.GetSessionAccountID now
  returns ErrStickySessionNotFound on a miss (mapped from redis.Nil in
  the repository implementation, mirroring ErrRefreshTokenNotFound) so
  the profit sticky path can distinguish "no binding yet" from a real
  read failure without importing the cache driver in service code
- cross-group re-entry (composite parent -> member group) resolves the
  gate against the member group and clears a stale parent gate instead
  of letting a foreign threshold veto accounts
- per-platform/group activity counters (installs, threshold vetoes,
  invalid-rate vetoes, refresh failures) for observability
- admin UI: profit-control section on the five platforms' group forms
  with percent input, validation and platform-switch reset; group
  create/update/duplicate normalize and validate the config at a
  single choke point
- cmd/profit-preview: offline what-if tool that replays the production
  admission semantics over an exported config/account/override/model
  dump, reports per-model admitted-account counts under the default
  and the worst-case (lowest user override) D, and surfaces probe-sync
  staleness as warnings without affecting admission

Tests: service unit coverage for gate resolution/veto/threshold
epsilon/pricing instant/suppress marker/scheduler filtering and
post-slot recheck (incl. -race on the profit surface), unit-tagged
handler slot-recheck and capability-mapping regressions, sqlmock and
real-PostgreSQL integration regressions for the GetByKeyForAuth
projection and the migration-192 trigger watch list, API contract
update, and frontend specs for the five-platform form helpers.
2026-08-01 22:39:31 +08:00
..
2025-12-18 13:50:39 +08:00
2025-12-18 13:50:39 +08:00
2026-02-02 22:13:50 +08:00
2026-02-02 22:13:50 +08:00

Database Migrations

Overview

This directory contains SQL migration files for database schema changes. The migration system uses SHA256 checksums to ensure migration immutability and consistency across environments.

Migration File Naming

Format: NNN_description.sql

  • NNN: Sequential number (e.g., 001, 002, 003)
  • description: Brief description in snake_case

Example: 017_add_gemini_tier_id.sql

_notx.sql 命名与执行语义(并发索引专用)

当迁移包含 CREATE INDEX CONCURRENTLY 或 DROP INDEX CONCURRENTLY 时,必须使用 _notx.sql 后缀,例如:

  • 062_add_accounts_priority_indexes_notx.sql
  • 063_drop_legacy_indexes_notx.sql

运行规则:

  1. *.sql(不带 _notx)按事务执行。
  2. *_notx.sql 按非事务执行,不会包裹在 BEGIN/COMMIT 中。
  3. *_notx.sql 仅允许并发索引语句,不允许混入事务控制语句或其他 DDL/DML。

幂等要求(必须):

  • 创建索引:CREATE INDEX CONCURRENTLY IF NOT EXISTS ...
  • 删除索引:DROP INDEX CONCURRENTLY IF EXISTS ...

这样可以保证灾备重放、重复执行时不会因对象已存在/不存在而失败。

Migration File Structure

This project uses a custom migration runner (internal/repository/migrations_runner.go) that executes the full SQL file content as-is.

  • Regular migrations (*.sql): executed in a transaction.
  • Non-transactional migrations (*_notx.sql): split by statement and executed without transaction (for CONCURRENTLY).
-- Forward-only migration (recommended)
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS example_column VARCHAR(100);

⚠️ Do not place executable "Down" SQL in the same file. The runner does not parse goose Up/Down sections and will execute all SQL statements in the file.

Important Rules

⚠️ Immutability Principle

Once a migration is applied to ANY environment (dev, staging, production), it MUST NOT be modified.

Why?

  • Each migration has a SHA256 checksum stored in the schema_migrations table
  • Modifying an applied migration causes checksum mismatch errors
  • Different environments would have inconsistent database states
  • Breaks audit trail and reproducibility

✅ Correct Workflow

  1. Create new migration

    # Create new file with next sequential number
    touch migrations/018_your_change.sql
    
  2. Write forward-only migration SQL

    • Put only the intended schema change in the file
    • If rollback is needed, create a new migration file to revert
  3. Test locally

    # Apply migration
    make migrate-up
    
    # Test rollback
    make migrate-down
    
  4. Commit and deploy

    git add migrations/018_your_change.sql
    git commit -m "feat(db): add your change"
    

❌ What NOT to Do

  • ❌ Modify an already-applied migration file
  • ❌ Delete migration files
  • ❌ Change migration file names
  • ❌ Reorder migration numbers

🔧 If You Accidentally Modified an Applied Migration

Error message:

migration 017_add_gemini_tier_id.sql checksum mismatch (db=abc123... file=def456...)

Solution:

# 1. Find the original version
git log --oneline -- migrations/017_add_gemini_tier_id.sql

# 2. Revert to the commit when it was first applied
git checkout <commit-hash> -- migrations/017_add_gemini_tier_id.sql

# 3. Create a NEW migration for your changes
touch migrations/018_your_new_change.sql

Migration System Details

  • Checksum Algorithm: SHA256 of trimmed file content
  • Tracking Table: schema_migrations (filename, checksum, applied_at)
  • Runner: internal/repository/migrations_runner.go
  • Auto-run: Migrations run automatically on service startup

Best Practices

  1. Keep migrations small and focused

    • One logical change per migration
    • Easier to review and rollback
  2. Write reversible migrations

    • Always provide a working Down migration
    • Test rollback before committing
  3. Use transactions

    • Wrap DDL statements in transactions when possible
    • Ensures atomicity
  4. Add comments

    • Explain WHY the change is needed
    • Document any special considerations
  5. Test in development first

    • Apply migration locally
    • Verify data integrity
    • Test rollback

Example Migration

-- Add tier_id field to Gemini OAuth accounts for quota tracking
UPDATE accounts
SET credentials = jsonb_set(
    credentials,
    '{tier_id}',
    '"LEGACY"',
    true
)
WHERE platform = 'gemini'
  AND type = 'oauth'
  AND credentials->>'tier_id' IS NULL;

Troubleshooting

Checksum Mismatch

See "If You Accidentally Modified an Applied Migration" above.

Migration Failed

# Check migration status
psql -d sub2api -c "SELECT * FROM schema_migrations ORDER BY applied_at DESC;"

# Manually rollback if needed (use with caution)
# Better to fix the migration and create a new one

Need to Skip a Migration (Emergency Only)

-- DANGEROUS: Only use in development or with extreme caution
INSERT INTO schema_migrations (filename, checksum, applied_at)
VALUES ('NNN_migration.sql', 'calculated_checksum', NOW());

References