vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
Index state changes for the activity feed
4d938fe894d56ff29b87fa0f2ebec50a744f2993 · Keenan Nemetz · 10d ago · parent e2a58f3a
Index state changes for the activity feed The activity feed found closed, reopened and merged events by reading the last 200 metadata commit subjects on every render: one git process per repository, so the instance home with N repositories ran N of them. It was the last derived read still going to git. - A `state_changes` table is fed by the same refresh: a delta reads the commits in its range, a rebuild reads the whole metadata branch. Rows are keyed by the commit, so a retried or overlapping refresh cannot duplicate an event, and an ordinal keeps two changes in one second in branch order (commit times are seconds; a close and a reopen often share one). - `Store.StateChanges` returns the events newest first; the feed reads it and keeps the metadata log as the fallback for a server without an index or a failed read. - `git.Repo.Log` and `CommitsBetween` treat a limit of zero as no limit, which the rebuild needs, and `metadata.ParseStateSubject` is exported so the index derives the events without a second copy of the format. The activity page now starts no metadata log; the instance home and the instance activity page lose one process per visible repository. On the load harness with three repositories: activity 496 req/s (p50 7 ms), instance activity 230 req/s (p50 17 ms), home 202 req/s (p50 20 ms). Schema version 7 rebuilds each repository on its first read after upgrade.
forge/index/activity.go +73
| @@ -0,0 +1,73 @@ | |||
| 1 | package index | ||
| 2 | |||
| 3 | // The activity feed's state changes: closed, reopened and merged events. | ||
| 4 | // | ||
| 5 | // The event is a metadata commit subject, so the refresh derives it from the | ||
| 6 | // commits in the range it is applying. The rows are keyed by commit, which is | ||
| 7 | // what makes an interrupted refresh safe to retry: the work it already did is | ||
| 8 | // ignored rather than duplicated. | ||
| 9 | |||
| 10 | import ( | ||
| 11 | "database/sql" | ||
| 12 | "time" | ||
| 13 | |||
| 14 | "github.com/rodent-software/vole/core/git" | ||
| 15 | "github.com/rodent-software/vole/metadata" | ||
| 16 | ) | ||
| 17 | |||
| 18 | // insertStateChanges records the events among a set of metadata commits, which | ||
| 19 | // arrive newest first. The ordinal is assigned oldest first so that two events in | ||
| 20 | // the same second keep the order the branch has them; commit times are seconds, | ||
| 21 | // and a close followed by a reopen often lands inside one. | ||
| 22 | func insertStateChanges(tx *sql.Tx, repoID string, commits []git.Commit) error { | ||
| 23 | var next int | ||
| 24 | if err := tx.QueryRow( | ||
| 25 | `SELECT COALESCE(MAX(ordinal), 0) FROM state_changes WHERE repo_id=?`, repoID).Scan(&next); err != nil { | ||
| 26 | return err | ||
| 27 | } | ||
| 28 | for i := len(commits) - 1; i >= 0; i-- { | ||
| 29 | c := commits[i] | ||
| 30 | kind, id, state, ok := metadata.ParseStateSubject(c.Subject) | ||
| 31 | if !ok { | ||
| 32 | continue | ||
| 33 | } | ||
| 34 | next++ | ||
| 35 | if _, err := tx.Exec( | ||
| 36 | `INSERT OR IGNORE INTO state_changes(repo_id, sha, kind, id, state, actor, at, ordinal) | ||
| 37 | VALUES(?,?,?,?,?,?,?,?)`, | ||
| 38 | repoID, c.SHA, kind, id, state, c.Author, c.When, next); err != nil { | ||
| 39 | return err | ||
| 40 | } | ||
| 41 | } | ||
| 42 | return nil | ||
| 43 | } | ||
| 44 | |||
| 45 | // StateChanges returns a repo's state changes newest first. A limit of zero | ||
| 46 | // returns all of them; the feed asks for a window, the rebuild needs none. | ||
| 47 | func (s *Store) StateChanges(repoID string, limit int) ([]metadata.StateChange, error) { | ||
| 48 | q := `SELECT kind, id, state, actor, at FROM state_changes | ||
| 49 | WHERE repo_id=? ORDER BY ordinal DESC` | ||
| 50 | args := []any{repoID} | ||
| 51 | if limit > 0 { | ||
| 52 | q += ` LIMIT ?` | ||
| 53 | args = append(args, limit) | ||
| 54 | } | ||
| 55 | rows, err := s.db.Query(q, args...) | ||
| 56 | if err != nil { | ||
| 57 | return nil, err | ||
| 58 | } | ||
| 59 | defer rows.Close() | ||
| 60 | var out []metadata.StateChange | ||
| 61 | for rows.Next() { | ||
| 62 | var ( | ||
| 63 | sc metadata.StateChange | ||
| 64 | at int64 | ||
| 65 | ) | ||
| 66 | if err := rows.Scan(&sc.Kind, &sc.ID, &sc.State, &sc.Actor, &at); err != nil { | ||
| 67 | return nil, err | ||
| 68 | } | ||
| 69 | sc.Time = time.Unix(at, 0).UTC() | ||
| 70 | out = append(out, sc) | ||
| 71 | } | ||
| 72 | return out, rows.Err() | ||
| 73 | } | ||