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.

Code

395 commit(s) on main

update readme Keenan Nemetz · 57m ago 32290114
history blame raw 6.2 KiB
1package web
2
3// What one page reads twice.
4//
5// A page and its header are built from the same facts: the repository, how many
6// issues and change requests are open, and the change requests themselves. Each
7// of those reads is git work, and the metadata branch grows with the number of
8// issues and changes, so asking for the same list twice is the difference
9// between a page that costs one read and one that costs two.
10//
11// The memo is per request and only for reads. A request that writes metadata
12// answers with a redirect, so it has nothing to memoise and cannot be served a
13// list it just invalidated; the next request sees the write. Nothing is cached
14// across requests, which is why a push is visible on the next page load without
15// a hook, a TTL, or an invalidation protocol.
16
17import (
18 "context"
19 "fmt"
20 "net/http"
21 "sync"
22
23 "rodent.software/vole/forge/index"
24 "rodent.software/vole/forge/repo"
25 "rodent.software/vole/metadata"
26)
27
28// memoKey is the context key for the request's memo. It is a private type so no
29// other package can install a value under it.
30type memoKey struct{}
31
32type memoCounts struct {
33 issues, requests int
34}
35
36type memoRequests struct {
37 list []*metadata.Request
38 err error
39}
40
41// memoHead is the metadata branch's commit as resolved for this request. The
42// index and the request list both need it, and it cannot move under a read
43// request without a push arriving mid-render.
44type memoHead struct {
45 sha string
46 err error
47}
48
49type requestMemo struct {
50 mu sync.Mutex
51 repos map[string]*repo.Meta
52 counts map[string]memoCounts
53 lists map[string]memoRequests
54 ensured map[string]error
55 heads map[string]memoHead
56}
57
58func newRequestMemo() *requestMemo {
59 return &requestMemo{
60 repos: map[string]*repo.Meta{},
61 counts: map[string]memoCounts{},
62 lists: map[string]memoRequests{},
63 ensured: map[string]error{},
64 heads: map[string]memoHead{},
65 }
66}
67
68// withMemo gives a read request one memo. A mutating request gets none: it may
69// have just changed what it would be reading.
70func withMemo(r *http.Request) *http.Request {
71 if r.Method != http.MethodGet && r.Method != http.MethodHead {
72 return r
73 }
74 return r.WithContext(context.WithValue(r.Context(), memoKey{}, newRequestMemo()))
75}
76
77// memoOf returns the request's memo, or nil when the request has none. Callers
78// treat nil as "read it directly", so a helper can be used from a write handler
79// that has no memo.
80func memoOf(r *http.Request) *requestMemo {
81 memo, _ := r.Context().Value(memoKey{}).(*requestMemo)
82 return memo
83}
84
85// repo opens a repository by name, once per request. Opening it resolves HEAD's
86// branch with a git call, and a page opens the same repository for its header,
87// its body and its ACL: the same fact does not need three processes.
88func (w *Web) repo(memo *requestMemo, name string) (*repo.Meta, error) {
89 if memo != nil {
90 memo.mu.Lock()
91 m, ok := memo.repos[name]
92 memo.mu.Unlock()
93 if ok {
94 return m, nil
95 }
96 }
97 m, err := w.Repos.Open(name)
98 if err != nil {
99 return nil, err
100 }
101 if memo != nil {
102 memo.mu.Lock()
103 memo.repos[name] = m
104 memo.mu.Unlock()
105 }
106 return m, nil
107}
108
109// metadataHead resolves the metadata branch once per request. Both the index
110// refresh and the change-request read start by resolving it, and a page needs
111// both.
112func (w *Web) metadataHead(memo *requestMemo, m *repo.Meta) (string, error) {
113 if memo == nil {
114 return m.Git().RefCommit(metadata.Ref)
115 }
116 memo.mu.Lock()
117 h, ok := memo.heads[m.Name]
118 memo.mu.Unlock()
119 if ok {
120 return h.sha, h.err
121 }
122 sha, err := m.Git().RefCommit(metadata.Ref)
123 memo.mu.Lock()
124 memo.heads[m.Name] = memoHead{sha: sha, err: err}
125 memo.mu.Unlock()
126 return sha, err
127}
128
129// ensureIndex refreshes the rows the repository derives from its metadata branch
130// at most once per request. Every surface that shows a number or a count asks
131// for it, and the refresh starts with a git call to see whether the branch
132// moved: the header, the issue list and the number in a URL should not each pay
133// for the same check on one page.
134func (w *Web) ensureIndex(memo *requestMemo, m *repo.Meta) error {
135 if memo == nil {
136 return w.Index.Ensure(m)
137 }
138 memo.mu.Lock()
139 err, ok := memo.ensured[m.Name]
140 memo.mu.Unlock()
141 if ok {
142 return err
143 }
144 head, err := w.metadataHead(memo, m)
145 if err == nil {
146 err = w.Index.EnsureAt(m, head)
147 }
148 memo.mu.Lock()
149 memo.ensured[m.Name] = err
150 memo.mu.Unlock()
151 return err
152}
153
154// requestsAt returns the change requests on the metadata branch, reading them
155// once per request. The list is the whole branch, so the header's open count and
156// the page's rows share one read rather than paying for the tree twice.
157func (w *Web) requestsAt(memo *requestMemo, m *repo.Meta) ([]*metadata.Request, error) {
158 if memo == nil {
159 return metadata.LoadRequests(m.Git(), metadata.Ref)
160 }
161 memo.mu.Lock()
162 entry, ok := memo.lists[m.Name]
163 memo.mu.Unlock()
164 if ok {
165 return entry.list, entry.err
166 }
167 head, err := w.metadataHead(memo, m)
168 var list []*metadata.Request
169 if err == nil {
170 list, err = metadata.LoadRequestsAt(m.Git(), head)
171 }
172 memo.mu.Lock()
173 memo.lists[m.Name] = memoRequests{list: list, err: err}
174 memo.mu.Unlock()
175 return list, err
176}
177
178// requestAt returns one change request for rendering. A read request uses the
179// index, which ensureIndex refreshes to the branch's current commit. A write
180// request reads git instead: it has no memo because it may have just changed the
181// record, and it must not be answered by the version it replaced.
182func (w *Web) requestAt(memo *requestMemo, m *repo.Meta, id string) (*metadata.Request, error) {
183 if w.Index != nil && memo != nil {
184 if err := w.ensureIndex(memo, m); err != nil {
185 return nil, err
186 }
187 return w.Index.Request(m.ID, id)
188 }
189 head, err := w.metadataHead(memo, m)
190 if err != nil {
191 return nil, err
192 }
193 req, found, err := metadata.LoadRequestAt(m.Git(), head, id)
194 if err != nil {
195 return nil, err
196 }
197 if !found {
198 return nil, fmt.Errorf("requests: no request %s", id)
199 }
200 return req, nil
201}
202
203// issueAt returns one issue for rendering, on the same rule: the index for a
204// read request, git for a write or a Web without an index.
205func (w *Web) issueAt(memo *requestMemo, m *repo.Meta, id string) (*metadata.Issue, error) {
206 if w.Index != nil && memo != nil {
207 if err := w.ensureIndex(memo, m); err != nil {
208 return nil, err
209 }
210 return w.Index.Issue(m.ID, id)
211 }
212 return index.LoadIssue(m, id)
213}
214