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.

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 {