Removes module-level doc comments (//! lines) and excessive inline doc comments that were duplicating information already evident from: - Function/struct names (self-documenting code) - Type signatures (the what is clear from types) - Implementation context (the how is clear from code) Affected modules: - cli/* - Removed command descriptions duplicating clap help text - core/* - Removed module headers and obvious function docs - documents/* - Removed extractor/regenerator/truncation docs - embedding/* - Removed pipeline and chunking docs - gitlab/* - Removed client and transformer docs (kept type definitions) - ingestion/* - Removed orchestrator and ingestion docs - search/* - Removed FTS and vector search docs Philosophy: Code should be self-documenting. Comments should explain "why" (business decisions, non-obvious constraints) not "what" (which the code itself shows). This change reduces noise and maintenance burden while keeping the codebase just as understandable. Retains comments for: - Non-obvious business logic - Important safety invariants - Complex algorithm explanations - Public API boundaries where generated docs matter Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
281 lines
8.7 KiB
Rust
281 lines
8.7 KiB
Rust
use rusqlite::Connection;
|
|
|
|
use crate::core::backoff::compute_next_attempt_at;
|
|
use crate::core::error::Result;
|
|
use crate::core::time::now_ms;
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum NoteableType {
|
|
Issue,
|
|
MergeRequest,
|
|
}
|
|
|
|
impl NoteableType {
|
|
pub fn as_str(&self) -> &'static str {
|
|
match self {
|
|
Self::Issue => "Issue",
|
|
Self::MergeRequest => "MergeRequest",
|
|
}
|
|
}
|
|
|
|
pub fn parse(s: &str) -> Option<Self> {
|
|
match s {
|
|
"Issue" => Some(Self::Issue),
|
|
"MergeRequest" => Some(Self::MergeRequest),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct PendingFetch {
|
|
pub project_id: i64,
|
|
pub noteable_type: NoteableType,
|
|
pub noteable_iid: i64,
|
|
pub attempt_count: i32,
|
|
}
|
|
|
|
pub fn queue_discussion_fetch(
|
|
conn: &Connection,
|
|
project_id: i64,
|
|
noteable_type: NoteableType,
|
|
noteable_iid: i64,
|
|
) -> Result<()> {
|
|
conn.execute(
|
|
"INSERT INTO pending_discussion_fetches (project_id, noteable_type, noteable_iid, queued_at)
|
|
VALUES (?1, ?2, ?3, ?4)
|
|
ON CONFLICT(project_id, noteable_type, noteable_iid) DO UPDATE SET
|
|
queued_at = excluded.queued_at,
|
|
attempt_count = 0,
|
|
last_attempt_at = NULL,
|
|
last_error = NULL,
|
|
next_attempt_at = NULL",
|
|
rusqlite::params![project_id, noteable_type.as_str(), noteable_iid, now_ms()],
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn get_pending_fetches(conn: &Connection, limit: usize) -> Result<Vec<PendingFetch>> {
|
|
let now = now_ms();
|
|
let mut stmt = conn.prepare(
|
|
"SELECT project_id, noteable_type, noteable_iid, attempt_count
|
|
FROM pending_discussion_fetches
|
|
WHERE next_attempt_at IS NULL OR next_attempt_at <= ?1
|
|
ORDER BY queued_at ASC
|
|
LIMIT ?2",
|
|
)?;
|
|
let rows = stmt
|
|
.query_map(rusqlite::params![now, limit as i64], |row| {
|
|
Ok((
|
|
row.get::<_, i64>(0)?,
|
|
row.get::<_, String>(1)?,
|
|
row.get::<_, i64>(2)?,
|
|
row.get::<_, i32>(3)?,
|
|
))
|
|
})?
|
|
.collect::<std::result::Result<Vec<_>, _>>()?;
|
|
|
|
let mut results = Vec::with_capacity(rows.len());
|
|
for (project_id, nt_str, noteable_iid, attempt_count) in rows {
|
|
let noteable_type = NoteableType::parse(&nt_str).ok_or_else(|| {
|
|
crate::core::error::LoreError::Other(format!(
|
|
"Invalid noteable_type in pending_discussion_fetches: {}",
|
|
nt_str
|
|
))
|
|
})?;
|
|
results.push(PendingFetch {
|
|
project_id,
|
|
noteable_type,
|
|
noteable_iid,
|
|
attempt_count,
|
|
});
|
|
}
|
|
Ok(results)
|
|
}
|
|
|
|
pub fn complete_fetch(
|
|
conn: &Connection,
|
|
project_id: i64,
|
|
noteable_type: NoteableType,
|
|
noteable_iid: i64,
|
|
) -> Result<()> {
|
|
conn.execute(
|
|
"DELETE FROM pending_discussion_fetches
|
|
WHERE project_id = ?1 AND noteable_type = ?2 AND noteable_iid = ?3",
|
|
rusqlite::params![project_id, noteable_type.as_str(), noteable_iid],
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn record_fetch_error(
|
|
conn: &Connection,
|
|
project_id: i64,
|
|
noteable_type: NoteableType,
|
|
noteable_iid: i64,
|
|
error: &str,
|
|
) -> Result<()> {
|
|
let now = now_ms();
|
|
let attempt_count: i64 = conn.query_row(
|
|
"SELECT attempt_count FROM pending_discussion_fetches
|
|
WHERE project_id = ?1 AND noteable_type = ?2 AND noteable_iid = ?3",
|
|
rusqlite::params![project_id, noteable_type.as_str(), noteable_iid],
|
|
|row| row.get(0),
|
|
)?;
|
|
|
|
let new_attempt = attempt_count + 1;
|
|
let next_at = compute_next_attempt_at(now, new_attempt);
|
|
|
|
conn.execute(
|
|
"UPDATE pending_discussion_fetches SET
|
|
attempt_count = ?1,
|
|
last_attempt_at = ?2,
|
|
last_error = ?3,
|
|
next_attempt_at = ?4
|
|
WHERE project_id = ?5 AND noteable_type = ?6 AND noteable_iid = ?7",
|
|
rusqlite::params![
|
|
new_attempt,
|
|
now,
|
|
error,
|
|
next_at,
|
|
project_id,
|
|
noteable_type.as_str(),
|
|
noteable_iid
|
|
],
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn setup_db() -> Connection {
|
|
let conn = Connection::open_in_memory().unwrap();
|
|
conn.execute_batch("
|
|
CREATE TABLE projects (
|
|
id INTEGER PRIMARY KEY,
|
|
gitlab_project_id INTEGER UNIQUE NOT NULL,
|
|
path_with_namespace TEXT NOT NULL,
|
|
default_branch TEXT,
|
|
web_url TEXT,
|
|
created_at INTEGER,
|
|
updated_at INTEGER,
|
|
raw_payload_id INTEGER
|
|
);
|
|
INSERT INTO projects (id, gitlab_project_id, path_with_namespace) VALUES (1, 100, 'group/project');
|
|
|
|
CREATE TABLE pending_discussion_fetches (
|
|
project_id INTEGER NOT NULL REFERENCES projects(id),
|
|
noteable_type TEXT NOT NULL,
|
|
noteable_iid INTEGER NOT NULL,
|
|
queued_at INTEGER NOT NULL,
|
|
attempt_count INTEGER NOT NULL DEFAULT 0,
|
|
last_attempt_at INTEGER,
|
|
last_error TEXT,
|
|
next_attempt_at INTEGER,
|
|
PRIMARY KEY(project_id, noteable_type, noteable_iid)
|
|
);
|
|
CREATE INDEX idx_pending_discussions_next_attempt ON pending_discussion_fetches(next_attempt_at);
|
|
").unwrap();
|
|
conn
|
|
}
|
|
|
|
#[test]
|
|
fn test_queue_and_get() {
|
|
let conn = setup_db();
|
|
queue_discussion_fetch(&conn, 1, NoteableType::Issue, 42).unwrap();
|
|
|
|
let fetches = get_pending_fetches(&conn, 100).unwrap();
|
|
assert_eq!(fetches.len(), 1);
|
|
assert_eq!(fetches[0].project_id, 1);
|
|
assert_eq!(fetches[0].noteable_type, NoteableType::Issue);
|
|
assert_eq!(fetches[0].noteable_iid, 42);
|
|
assert_eq!(fetches[0].attempt_count, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_requeue_resets_backoff() {
|
|
let conn = setup_db();
|
|
queue_discussion_fetch(&conn, 1, NoteableType::Issue, 42).unwrap();
|
|
record_fetch_error(&conn, 1, NoteableType::Issue, 42, "network error").unwrap();
|
|
|
|
let attempt: i32 = conn
|
|
.query_row(
|
|
"SELECT attempt_count FROM pending_discussion_fetches WHERE noteable_iid = 42",
|
|
[],
|
|
|r| r.get(0),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(attempt, 1);
|
|
|
|
queue_discussion_fetch(&conn, 1, NoteableType::Issue, 42).unwrap();
|
|
let attempt: i32 = conn
|
|
.query_row(
|
|
"SELECT attempt_count FROM pending_discussion_fetches WHERE noteable_iid = 42",
|
|
[],
|
|
|r| r.get(0),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(attempt, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_backoff_respected() {
|
|
let conn = setup_db();
|
|
queue_discussion_fetch(&conn, 1, NoteableType::Issue, 42).unwrap();
|
|
conn.execute(
|
|
"UPDATE pending_discussion_fetches SET next_attempt_at = 9999999999999 WHERE noteable_iid = 42",
|
|
[],
|
|
).unwrap();
|
|
|
|
let fetches = get_pending_fetches(&conn, 100).unwrap();
|
|
assert!(fetches.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_complete_removes() {
|
|
let conn = setup_db();
|
|
queue_discussion_fetch(&conn, 1, NoteableType::Issue, 42).unwrap();
|
|
complete_fetch(&conn, 1, NoteableType::Issue, 42).unwrap();
|
|
|
|
let count: i64 = conn
|
|
.query_row("SELECT COUNT(*) FROM pending_discussion_fetches", [], |r| {
|
|
r.get(0)
|
|
})
|
|
.unwrap();
|
|
assert_eq!(count, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_error_increments_attempts() {
|
|
let conn = setup_db();
|
|
queue_discussion_fetch(&conn, 1, NoteableType::MergeRequest, 10).unwrap();
|
|
record_fetch_error(&conn, 1, NoteableType::MergeRequest, 10, "timeout").unwrap();
|
|
|
|
let (attempt, error): (i32, Option<String>) = conn.query_row(
|
|
"SELECT attempt_count, last_error FROM pending_discussion_fetches WHERE noteable_iid = 10",
|
|
[], |r| Ok((r.get(0)?, r.get(1)?)),
|
|
).unwrap();
|
|
assert_eq!(attempt, 1);
|
|
assert_eq!(error, Some("timeout".to_string()));
|
|
|
|
let next_at: Option<i64> = conn
|
|
.query_row(
|
|
"SELECT next_attempt_at FROM pending_discussion_fetches WHERE noteable_iid = 10",
|
|
[],
|
|
|r| r.get(0),
|
|
)
|
|
.unwrap();
|
|
assert!(next_at.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn test_noteable_type_parse() {
|
|
assert_eq!(NoteableType::parse("Issue"), Some(NoteableType::Issue));
|
|
assert_eq!(
|
|
NoteableType::parse("MergeRequest"),
|
|
Some(NoteableType::MergeRequest)
|
|
);
|
|
assert_eq!(NoteableType::parse("invalid"), None);
|
|
}
|
|
}
|