Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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 }