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/activity.go +49−19
| @@ -374,6 +374,30 @@ func (w *Web) activityRequests(memo *requestMemo, m *repo.Meta) []activityReques | |||
| 374 | 374 | return out | |
| 375 | 375 | } | |
| 376 | 376 | ||
| 377 | // repoActivityURL builds a page link for a repository's feed, keeping the | ||
| 378 | // filters so paging never widens what the reader asked for. | ||
| 379 | func repoActivityURL(name, kind, actor, assignee string) func(int) string { | ||
| 380 | return func(n int) string { | ||
| 381 | v := url.Values{} | ||
| 382 | if kind != "" { | ||
| 383 | v.Set("kind", kind) | ||
| 384 | } | ||
| 385 | if actor != "" { | ||
| 386 | v.Set("actor", actor) | ||
| 387 | } | ||
| 388 | if assignee != "" { | ||
| 389 | v.Set("assignee", assignee) | ||
| 390 | } | ||
| 391 | if n > 1 { | ||
| 392 | v.Set("page", strconv.Itoa(n)) | ||
| 393 | } | ||
| 394 | if q := v.Encode(); q != "" { | ||
| 395 | return "/r/" + name + "/activity?" + q | ||
| 396 | } | ||
| 397 | return "/r/" + name + "/activity" | ||
| 398 | } | ||
| 399 | } | ||
| 400 | |||
| 377 | 401 | // activityHistory is how far back the feed looks for state changes. The feed | |
| 378 | 402 | // itself keeps fewer events; this is the window a closed issue has to be seen in. | |
| 379 | 403 | const activityHistory = 200 | |
| @@ -450,32 +474,38 @@ func (w *Web) Activity(rw http.ResponseWriter, r *http.Request) { | |||
| 450 | 474 | kind := normalizeActivityKind(r.URL.Query().Get("kind")) | |
| 451 | 475 | actor := strings.TrimSpace(r.URL.Query().Get("actor")) | |
| 452 | 476 | assignee := strings.TrimSpace(r.URL.Query().Get("assignee")) | |
| 453 | feed := w.repoActivity(memoOf(r), m, kind, actor, assignee, 0) | ||
| 454 | 477 | number, _ := strconv.Atoi(r.URL.Query().Get("page")) | |
| 455 | events, pages := paginate(feed.Events, number, activityPage, func(n int) string { | ||
| 456 | v := url.Values{} | ||
| 457 | if kind != "" { | ||
| 458 | v.Set("kind", kind) | ||
| 459 | } | ||
| 460 | if actor != "" { | ||
| 461 | v.Set("actor", actor) | ||
| 462 | } | ||
| 463 | if assignee != "" { | ||
| 464 | v.Set("assignee", assignee) | ||
| 465 | } | ||
| 466 | if n > 1 { | ||
| 467 | v.Set("page", strconv.Itoa(n)) | ||
| 478 | urlFor := repoActivityURL(m.Name, kind, actor, assignee) | ||
| 479 | |||
| 480 | var events []activityEvent | ||
| 481 | var pages pager | ||
| 482 | var assignees []string | ||
| 483 | if w.Index != nil { | ||
| 484 | if err := w.ensureIndex(memoOf(r), m); err != nil { | ||
| 485 | w.serverError(rw, r, "cannot read activity: "+err.Error()) | ||
| 486 | return | ||
| 468 | 487 | } | |
| 469 | if q := v.Encode(); q != "" { | ||
| 470 | return "/r/" + m.Name + "/activity?" + q | ||
| 488 | var err error | ||
| 489 | events, pages, err = w.activityPage( | ||
| 490 | []string{m.ID}, []*repo.Meta{m}, map[string]string{m.ID: m.Name}, | ||
| 491 | kind, actor, assignee, false, number, activityPage, urlFor, "Activity pages") | ||
| 492 | if err != nil { | ||
| 493 | w.serverError(rw, r, "cannot read activity: "+err.Error()) | ||
| 494 | return | ||
| 471 | 495 | } | |
| 472 | return "/r/" + m.Name + "/activity" | ||
| 473 | }, "Activity pages") | ||
| 496 | assignees, _ = w.Index.Assignees(m.ID) | ||
| 497 | } else { | ||
| 498 | // A Web without an index (a test, or a CLI-shaped Web) builds the feed | ||
| 499 | // in memory, as every page used to. | ||
| 500 | feed := w.repoActivity(memoOf(r), m, kind, actor, assignee, 0) | ||
| 501 | events, pages = paginate(feed.Events, number, activityPage, urlFor, "Activity pages") | ||
| 502 | assignees = feed.Assignees | ||
| 503 | } | ||
| 474 | 504 | pages.Target = "#activity-feed" | |
| 475 | 505 | view := activityView{ | |
| 476 | 506 | Page: w.page(r, m.Name+" · activity"), Name: m.Name, Kind: kind, | |
| 477 | 507 | Actor: actor, Assignee: assignee, | |
| 478 | Actors: w.activityActors(), Assignees: feed.Assignees, Events: events, Pager: pages, | ||
| 508 | Actors: w.activityActors(), Assignees: assignees, Events: events, Pager: pages, | ||
| 479 | 509 | } | |
| 480 | 510 | if htmxRequest(r) { | |
| 481 | 511 | w.render(rw, "activity_feed", view, 0) | |