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.
Read the activity feed as a SQL stream
73a732b1b84a308f0739e4c8325e9c2a832097cb · Keenan Nemetz · 10d ago · parent c8cdb6ac
Read the activity feed as a SQL stream A page of the feed used to be built in memory: every issue, comment, approval and state change became an event, was sorted, and was then sliced to fifty rows. The page cost the whole history, and the instance feed did it for every visible repository. The sources are rows in the index, so the stream is a UNION over them (forge/index.Activity and ActivityCount): the database filters, orders and limits, and only the page's rows cross back into the process. Releases are the one event source that is not an index row — a tag is a ref — so they are read from git (one call per repository, only when the feed can show a release) and merged with the page in time order. The handlers refresh each repository before reading, because a browser write never passes through the push hook. The repository feed keeps the in-memory path as a fallback for a Web without an index. `visibleRepos` now builds a repository handle without a HEAD lookup, which removes one git process per repository from the instance activity page too. The count and the page each scan the union, so the database still sorts the events rather than reading an index in order; a materialised events table is the next step if a large history makes that visible. What is gone is the per-request materialisation of every event in Go, which is what this follow-up set out to remove.
server/web/home.go +32−25
| @@ -18,6 +18,7 @@ import ( | |||
| 18 | 18 | "github.com/rodent-software/vole/core/git" | |
| 19 | 19 | "github.com/rodent-software/vole/forge/access" | |
| 20 | 20 | "github.com/rodent-software/vole/forge/index" | |
| 21 | "github.com/rodent-software/vole/forge/repo" | ||
| 21 | 22 | ) | |
| 22 | 23 | ||
| 23 | 24 | const ( | |
| @@ -119,8 +120,37 @@ func (w *Web) InstanceActivity(rw http.ResponseWriter, r *http.Request) { | |||
| 119 | 120 | return | |
| 120 | 121 | } | |
| 121 | 122 | number, _ := strconv.Atoi(r.URL.Query().Get("page")) | |
| 122 | events, pager := paginate(w.instanceEvents(memoOf(r), entries, kind), number, instanceEventPage, | ||
| 123 | instanceActivityURL(kind), "Activity pages") | ||
| 123 | if w.Index == nil { | ||
| 124 | w.serverError(rw, r, "the activity feed is unavailable right now") | ||
| 125 | return | ||
| 126 | } | ||
| 127 | repoIDs := make([]string, 0, len(entries)) | ||
| 128 | names := make(map[string]string, len(entries)) | ||
| 129 | metas := make([]*repo.Meta, 0, len(entries)) | ||
| 130 | memo := memoOf(r) | ||
| 131 | for _, e := range entries { | ||
| 132 | m, err := w.Repos.Meta(e.Name) | ||
| 133 | if err != nil { | ||
| 134 | continue | ||
| 135 | } | ||
| 136 | // The feed reads the index directly, so each repository it can show has | ||
| 137 | // to be current: a browser write never passes through the push hook. | ||
| 138 | if err := w.ensureIndex(memo, m); err != nil { | ||
| 139 | if w.Logger != nil { | ||
| 140 | w.Logger("web: activity index for %s: %v", e.Name, err) | ||
| 141 | } | ||
| 142 | continue | ||
| 143 | } | ||
| 144 | repoIDs = append(repoIDs, e.ID) | ||
| 145 | names[e.ID] = e.Name | ||
| 146 | metas = append(metas, m) | ||
| 147 | } | ||
| 148 | events, pager, err := w.activityPage(repoIDs, metas, names, kind, "", "", true, | ||
| 149 | number, instanceEventPage, instanceActivityURL(kind), "Activity pages") | ||
| 150 | if err != nil { | ||
| 151 | w.serverError(rw, r, "cannot read activity: "+err.Error()) | ||
| 152 | return | ||
| 153 | } | ||
| 124 | 154 | w.render(rw, "instance_activity", instanceActivityData{ | |
| 125 | 155 | Page: w.page(r, "Activity"), Kind: kind, Events: events, Pager: pager, | |
| 126 | 156 | }, 0) | |
| @@ -197,29 +227,6 @@ func (w *Web) repoCards(memo *requestMemo, entries []index.Entry) ([]repoCard, [ | |||
| 197 | 227 | return cards, events | |
| 198 | 228 | } | |
| 199 | 229 | ||
| 200 | // instanceEvents merges the visible repositories' feeds for one kind. It is | ||
| 201 | // bounded after the merge: a per-repository cap would hide a quiet repository's | ||
| 202 | // only event on an instance with a busy neighbour. | ||
| 203 | func (w *Web) instanceEvents(memo *requestMemo, entries []index.Entry, kind string) []activityEvent { | ||
| 204 | var events []activityEvent | ||
| 205 | for _, e := range entries { | ||
| 206 | m, err := w.repo(memo, e.Name) | ||
| 207 | if err != nil { | ||
| 208 | continue | ||
| 209 | } | ||
| 210 | for _, ev := range w.repoActivity(memo, m, kind, "", "", 0).Events { | ||
| 211 | ev.Repo = m.Name | ||
| 212 | events = append(events, ev) | ||
| 213 | } | ||
| 214 | } | ||
| 215 | sort.SliceStable(events, func(i, j int) bool { return events[i].Time.After(events[j].Time) }) | ||
| 216 | return events | ||
| 217 | } | ||
| 218 | |||
| 219 | // paginate slices one page out of the merged feed. A page number outside the | ||
| 220 | // range is clamped to the nearest page, so an old link or a hand-edited URL still | ||
| 221 | // renders something rather than an empty page. The kind filter travels with the | ||
| 222 | // page links, so paging never silently widens the feed. | ||
| 223 | 230 | // instanceActivityURL builds a page link for /activity, keeping the kind filter | |
| 224 | 231 | // so paging never silently widens the feed. | |
| 225 | 232 | func instanceActivityURL(kind string) func(int) string { | |