vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Materialise the activity feed in an events table
bca9ec24cbbb938f2d349d5f06e2c1f4fca06fcb · Keenan Nemetz · 9d ago · parent 73a732b1
Materialise the activity feed in an events table The previous commit read the feed as a UNION over the source tables. It stopped materialising events in Go, but SQLite still scanned and sorted the union to find a page, so a page cost the history. This replaces it with the design the feed actually wants: an `events` table with one row per event — issue and change opened, comment, approval, state change, release — carrying the time, actor, title and assignees the feed renders. Rows are indexed by (repository, time, id), so a repository page is an index range read; the instance page walks the global time index and filters by repository, and neither sorts history. An unfiltered page total is a stored per-repository count. Maintenance is part of the refresh that already exists: - an entity's events are replaced when the entity is reloaded, which is how a title or assignee edit reaches old events; - state changes are rebuilt into events for the entities the commits name, which also catches a close-and-reopen that leaves the tree where it started and an endpoint diff cannot see; - releases are refreshed from the tags after every push (post-receive) and after a release written in the browser, because a tag is a ref and no metadata commit announces it; - the stored count moves with the rows a refresh adds or removes. Measured with 100,000 events across 50 repositories: a repository page takes 0.3-0.5 ms (page 1 or page 20), an instance page 0.5-0.9 ms, and an unfiltered total 0.1 ms. A filtered total is the one query that scans, at 12 ms for 100,000 events. On the load harness, instance activity rises from 222 to 482 req/s and p50 from 18 ms to 8 ms; activity from 577 to 762 req/s. The repository feed no longer reads tags per request, and nothing reads the metadata branch to render a page.
server/web/release_actions.go +14
| @@ -28,6 +28,18 @@ type releaseFormView struct { | |||
| 28 | 28 | Error string | |
| 29 | 29 | } | |
| 30 | 30 | ||
| 31 | // refreshReleaseEvents brings the feed's release rows up to date after the | ||
| 32 | // browser writes a tag directly on the server. A failure is logged, never | ||
| 33 | // reported over the release that already exists. | ||
| 34 | func (w *Web) refreshReleaseEvents(m *repo.Meta) { | ||
| 35 | if w.Index == nil { | ||
| 36 | return | ||
| 37 | } | ||
| 38 | if err := w.Index.RefreshTags(m); err != nil && w.Logger != nil { | ||
| 39 | w.Logger("web: refresh release events for %s: %v", m.Name, err) | ||
| 40 | } | ||
| 41 | } | ||
| 42 | |||
| 31 | 43 | // canManageReleases is the read-side half of the release writes: the pages that | |
| 32 | 44 | // offer the form and the handlers that accept it ask the same capability. | |
| 33 | 45 | func (w *Web) canManageReleases(r *http.Request, m *repo.Meta) bool { | |
| @@ -112,6 +124,7 @@ func (w *Web) CreateRelease(rw http.ResponseWriter, r *http.Request) { | |||
| 112 | 124 | fail(err.Error()) | |
| 113 | 125 | return | |
| 114 | 126 | } | |
| 127 | w.refreshReleaseEvents(m) | ||
| 115 | 128 | w.audit("web.release.create", ctx.Name, m.Name, tag) | |
| 116 | 129 | http.Redirect(rw, r, "/r/"+m.Name+"/releases/"+pathURL(tag), http.StatusSeeOther) | |
| 117 | 130 | } | |
| @@ -187,6 +200,7 @@ func (w *Web) UpdateRelease(rw http.ResponseWriter, r *http.Request) { | |||
| 187 | 200 | fail(err.Error()) | |
| 188 | 201 | return | |
| 189 | 202 | } | |
| 203 | w.refreshReleaseEvents(m) | ||
| 190 | 204 | w.audit("web.release.edit", ctx.Name, m.Name, t.Name) | |
| 191 | 205 | http.Redirect(rw, r, "/r/"+m.Name+"/releases/"+pathURL(t.Name), http.StatusSeeOther) | |
| 192 | 206 | } | |