-
Notifications
You must be signed in to change notification settings - Fork 318
perf: reduce homepage database hot paths and chunk large key queries #287
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
mozi1924
wants to merge
4
commits into
qxcnm:main
Choose a base branch
from
mozi1924:codex/homepage-db-hotpath-fix
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
0da87fc
fix: push homepage hot-path filtering into storage queries
mozi1924 845e4da
fix: chunk large key-list storage queries to avoid SQLite limits
mozi1924 1d3e9e5
fix: address dashboard hot path review feedback
mozi1924 4118c05
fix: optimize member dashboard request log filters
mozi1924 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,125 +1,164 @@ | ||
| use std::collections::HashMap; | ||
|
|
||
| use rusqlite::{OptionalExtension, Result}; | ||
|
|
||
| use super::{now_ts, Storage}; | ||
|
|
||
| impl Storage { | ||
| pub fn upsert_api_key_quota_limit( | ||
| &self, | ||
| key_id: &str, | ||
| quota_limit_tokens: Option<i64>, | ||
| ) -> Result<()> { | ||
| let normalized = quota_limit_tokens.filter(|value| *value > 0); | ||
| let Some(limit) = normalized else { | ||
| self.conn.execute( | ||
| "DELETE FROM api_key_quota_limits WHERE key_id = ?1", | ||
| [key_id], | ||
| )?; | ||
| return Ok(()); | ||
| }; | ||
|
|
||
| let now = now_ts(); | ||
| self.conn.execute( | ||
| "INSERT INTO api_key_quota_limits ( | ||
| key_id, quota_limit_tokens, created_at, updated_at | ||
| ) VALUES (?1, ?2, ?3, ?3) | ||
| ON CONFLICT(key_id) DO UPDATE SET | ||
| quota_limit_tokens = excluded.quota_limit_tokens, | ||
| updated_at = excluded.updated_at", | ||
| (key_id, limit, now), | ||
| )?; | ||
| Ok(()) | ||
| } | ||
|
|
||
| pub fn find_api_key_quota_limit(&self, key_id: &str) -> Result<Option<i64>> { | ||
| self.conn | ||
| .query_row( | ||
| "SELECT quota_limit_tokens | ||
| FROM api_key_quota_limits | ||
| WHERE key_id = ?1 | ||
| LIMIT 1", | ||
| [key_id], | ||
| |row| row.get(0), | ||
| ) | ||
| .optional() | ||
| } | ||
|
|
||
| pub fn list_api_key_quota_limits(&self) -> Result<HashMap<String, i64>> { | ||
| let mut stmt = self.conn.prepare( | ||
| "SELECT key_id, quota_limit_tokens | ||
| FROM api_key_quota_limits | ||
| WHERE quota_limit_tokens > 0", | ||
| )?; | ||
| let mut rows = stmt.query([])?; | ||
| let mut out = HashMap::new(); | ||
| while let Some(row) = rows.next()? { | ||
| out.insert(row.get(0)?, row.get(1)?); | ||
| } | ||
| Ok(out) | ||
| } | ||
|
|
||
| pub fn api_key_total_token_usage(&self, key_id: &str) -> Result<i64> { | ||
| let mut stmt = self.conn.prepare( | ||
| "WITH all_stats AS ( | ||
| SELECT | ||
| key_id, | ||
| input_tokens, | ||
| cached_input_tokens, | ||
| output_tokens, | ||
| total_tokens | ||
| FROM request_token_stats | ||
| UNION ALL | ||
| SELECT | ||
| NULLIF(key_id, '') AS key_id, | ||
| input_tokens, | ||
| cached_input_tokens, | ||
| output_tokens, | ||
| total_tokens | ||
| FROM request_token_stat_rollups | ||
| ) | ||
| SELECT | ||
| IFNULL( | ||
| SUM( | ||
| CASE | ||
| WHEN total_tokens IS NOT NULL THEN | ||
| CASE WHEN total_tokens > 0 THEN total_tokens ELSE 0 END | ||
| ELSE | ||
| CASE | ||
| WHEN IFNULL(input_tokens, 0) - IFNULL(cached_input_tokens, 0) + IFNULL(output_tokens, 0) > 0 | ||
| THEN IFNULL(input_tokens, 0) - IFNULL(cached_input_tokens, 0) + IFNULL(output_tokens, 0) | ||
| ELSE 0 | ||
| END | ||
| END | ||
| ), | ||
| 0 | ||
| ) AS total_tokens | ||
| FROM all_stats | ||
| WHERE key_id = ?1", | ||
| )?; | ||
| let mut rows = stmt.query([key_id])?; | ||
| if let Some(row) = rows.next()? { | ||
| let total: i64 = row.get(0)?; | ||
| return Ok(total.max(0)); | ||
| } | ||
| Ok(0) | ||
| } | ||
|
|
||
| pub(super) fn ensure_api_key_quota_limits_table(&self) -> Result<()> { | ||
| self.conn.execute( | ||
| "CREATE TABLE IF NOT EXISTS api_key_quota_limits ( | ||
| key_id TEXT PRIMARY KEY REFERENCES api_keys(id) ON DELETE CASCADE, | ||
| quota_limit_tokens INTEGER NOT NULL, | ||
| created_at INTEGER NOT NULL, | ||
| updated_at INTEGER NOT NULL | ||
| )", | ||
| [], | ||
| )?; | ||
| self.conn.execute( | ||
| "CREATE INDEX IF NOT EXISTS idx_api_key_quota_limits_updated_at | ||
| ON api_key_quota_limits(updated_at DESC)", | ||
| [], | ||
| )?; | ||
| Ok(()) | ||
| } | ||
| } | ||
| use std::collections::HashMap; | ||
|
|
||
| use rusqlite::{params_from_iter, OptionalExtension, Result}; | ||
|
|
||
| use super::key_id_filters::{key_id_in_clause, normalize_key_ids, SQLITE_IN_CLAUSE_BATCH_SIZE}; | ||
| use super::{now_ts, Storage}; | ||
|
|
||
| impl Storage { | ||
| pub fn upsert_api_key_quota_limit( | ||
| &self, | ||
| key_id: &str, | ||
| quota_limit_tokens: Option<i64>, | ||
| ) -> Result<()> { | ||
| let normalized = quota_limit_tokens.filter(|value| *value > 0); | ||
| let Some(limit) = normalized else { | ||
| self.conn.execute( | ||
| "DELETE FROM api_key_quota_limits WHERE key_id = ?1", | ||
| [key_id], | ||
| )?; | ||
| return Ok(()); | ||
| }; | ||
|
|
||
| let now = now_ts(); | ||
| self.conn.execute( | ||
| "INSERT INTO api_key_quota_limits ( | ||
| key_id, quota_limit_tokens, created_at, updated_at | ||
| ) VALUES (?1, ?2, ?3, ?3) | ||
| ON CONFLICT(key_id) DO UPDATE SET | ||
| quota_limit_tokens = excluded.quota_limit_tokens, | ||
| updated_at = excluded.updated_at", | ||
| (key_id, limit, now), | ||
| )?; | ||
| Ok(()) | ||
| } | ||
|
|
||
| pub fn find_api_key_quota_limit(&self, key_id: &str) -> Result<Option<i64>> { | ||
| self.conn | ||
| .query_row( | ||
| "SELECT quota_limit_tokens | ||
| FROM api_key_quota_limits | ||
| WHERE key_id = ?1 | ||
| LIMIT 1", | ||
| [key_id], | ||
| |row| row.get(0), | ||
| ) | ||
| .optional() | ||
| } | ||
|
|
||
| pub fn list_api_key_quota_limits(&self) -> Result<HashMap<String, i64>> { | ||
| let mut stmt = self.conn.prepare( | ||
| "SELECT key_id, quota_limit_tokens | ||
| FROM api_key_quota_limits | ||
| WHERE quota_limit_tokens > 0", | ||
| )?; | ||
| let mut rows = stmt.query([])?; | ||
| let mut out = HashMap::new(); | ||
| while let Some(row) = rows.next()? { | ||
| out.insert(row.get(0)?, row.get(1)?); | ||
| } | ||
| Ok(out) | ||
| } | ||
|
|
||
| pub fn list_api_key_quota_limits_for_ids( | ||
| &self, | ||
| key_ids: &[String], | ||
| ) -> Result<HashMap<String, i64>> { | ||
| let key_ids = normalize_key_ids(key_ids); | ||
| if key_ids.is_empty() { | ||
| return Ok(HashMap::new()); | ||
| } | ||
|
|
||
| let mut out = HashMap::new(); | ||
| for chunk in key_ids.chunks(SQLITE_IN_CLAUSE_BATCH_SIZE) { | ||
| out.extend(list_api_key_quota_limits_for_ids_chunk(self, chunk)?); | ||
| } | ||
| Ok(out) | ||
| } | ||
|
|
||
| pub fn api_key_total_token_usage(&self, key_id: &str) -> Result<i64> { | ||
| let mut stmt = self.conn.prepare( | ||
| "WITH all_stats AS ( | ||
| SELECT | ||
| key_id, | ||
| input_tokens, | ||
| cached_input_tokens, | ||
| output_tokens, | ||
| total_tokens | ||
| FROM request_token_stats | ||
| UNION ALL | ||
| SELECT | ||
| NULLIF(key_id, '') AS key_id, | ||
| input_tokens, | ||
| cached_input_tokens, | ||
| output_tokens, | ||
| total_tokens | ||
| FROM request_token_stat_rollups | ||
| ) | ||
| SELECT | ||
| IFNULL( | ||
| SUM( | ||
| CASE | ||
| WHEN total_tokens IS NOT NULL THEN | ||
| CASE WHEN total_tokens > 0 THEN total_tokens ELSE 0 END | ||
| ELSE | ||
| CASE | ||
| WHEN IFNULL(input_tokens, 0) - IFNULL(cached_input_tokens, 0) + IFNULL(output_tokens, 0) > 0 | ||
| THEN IFNULL(input_tokens, 0) - IFNULL(cached_input_tokens, 0) + IFNULL(output_tokens, 0) | ||
| ELSE 0 | ||
| END | ||
| END | ||
| ), | ||
| 0 | ||
| ) AS total_tokens | ||
| FROM all_stats | ||
| WHERE key_id = ?1", | ||
| )?; | ||
| let mut rows = stmt.query([key_id])?; | ||
| if let Some(row) = rows.next()? { | ||
| let total: i64 = row.get(0)?; | ||
| return Ok(total.max(0)); | ||
| } | ||
| Ok(0) | ||
| } | ||
|
|
||
| pub(super) fn ensure_api_key_quota_limits_table(&self) -> Result<()> { | ||
| self.conn.execute( | ||
| "CREATE TABLE IF NOT EXISTS api_key_quota_limits ( | ||
| key_id TEXT PRIMARY KEY REFERENCES api_keys(id) ON DELETE CASCADE, | ||
| quota_limit_tokens INTEGER NOT NULL, | ||
| created_at INTEGER NOT NULL, | ||
| updated_at INTEGER NOT NULL | ||
| )", | ||
| [], | ||
| )?; | ||
| self.conn.execute( | ||
| "CREATE INDEX IF NOT EXISTS idx_api_key_quota_limits_updated_at | ||
| ON api_key_quota_limits(updated_at DESC)", | ||
| [], | ||
| )?; | ||
| Ok(()) | ||
| } | ||
| } | ||
|
|
||
| fn list_api_key_quota_limits_for_ids_chunk( | ||
| storage: &Storage, | ||
| key_ids: &[String], | ||
| ) -> Result<HashMap<String, i64>> { | ||
| let Some((clause, params)) = key_id_in_clause("key_id", key_ids) else { | ||
| return Ok(HashMap::new()); | ||
| }; | ||
| let sql = format!( | ||
| "SELECT key_id, quota_limit_tokens | ||
| FROM api_key_quota_limits | ||
| WHERE quota_limit_tokens > 0 | ||
| AND {clause}" | ||
| ); | ||
| let mut stmt = storage.conn.prepare(&sql)?; | ||
| let mut rows = stmt.query(params_from_iter(params.iter()))?; | ||
| let mut out = HashMap::new(); | ||
| while let Some(row) = rows.next()? { | ||
| out.insert(row.get(0)?, row.get(1)?); | ||
| } | ||
| Ok(out) | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这里新增了按
owner_kind + owner_user_id查询 key id 的索引路径,但服务层实际使用的crate::list_api_key_ids_for_user仍然调用storage.list_api_key_owners()拉全量 owner 后在 Rust 里过滤。因此成员 Dashboard、startup snapshot、API Key 页面仍会走旧热点路径,PR 的主要优化没有真正生效。建议把服务层 helper 改为调用这个新的 storage 方法,并补一个覆盖非 admin 路径的测试。