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/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.

Blame · server/web/memo.go

Line attribution on main · 213 line(s)

5d440e37
Keenan Nemetz
10d ago
1 package 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
17 import (
18 "context"
19 "fmt"
20 "net/http"
21 "sync"
22
d37d301f
Keenan Nemetz
16h ago
23 "rodent.software/vole/forge/index"
24 "rodent.software/vole/forge/repo"
25 "rodent.software/vole/metadata"
5d440e37
Keenan Nemetz
10d ago
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.
30 type memoKey struct{}
31
32 type memoCounts struct {
33 issues, requests int
34 }
35
36 type memoRequests struct {
37 list []*metadata.Request
38 err error
39 }
40
73ec800b
Keenan Nemetz
10d ago
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.
44 type memoHead struct {
45 sha string
46 err error
47 }
48
5d440e37
Keenan Nemetz
10d ago
49 type requestMemo struct {
73ec800b
Keenan Nemetz
10d ago
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
5d440e37
Keenan Nemetz
10d ago
56 }
57
58 func newRequestMemo() *requestMemo {
59 return &requestMemo{
73ec800b
Keenan Nemetz
10d ago
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{},
5d440e37
Keenan Nemetz
10d ago
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.
70 func 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.
80 func 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.
88 func (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
73ec800b
Keenan Nemetz
10d ago
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.
112 func (w *Web) metadataHead(memo *requestMemo, m *repo.Meta) (string, error) {
113 if memo == nil {
dced06da
Keenan Nemetz
9d ago
114 return m.Git().RefCommit(metadata.Ref)
73ec800b
Keenan Nemetz
10d ago
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 }
dced06da
Keenan Nemetz
9d ago
122 sha, err := m.Git().RefCommit(metadata.Ref)
73ec800b
Keenan Nemetz
10d ago
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.
134 func (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
5d440e37
Keenan Nemetz
10d ago
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.
157 func (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 }
73ec800b
Keenan Nemetz
10d ago
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 }
5d440e37
Keenan Nemetz
10d ago
172 memo.mu.Lock()
173 memo.lists[m.Name] = memoRequests{list: list, err: err}
174 memo.mu.Unlock()
175 return list, err
176 }
177
14b5c648
Keenan Nemetz
9d ago
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.
5d440e37
Keenan Nemetz
10d ago
182 func (w *Web) requestAt(memo *requestMemo, m *repo.Meta, id string) (*metadata.Request, error) {
14b5c648
Keenan Nemetz
9d ago
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 }
b739915c
Keenan Nemetz
10d ago
189 head, err := w.metadataHead(memo, m)
5d440e37
Keenan Nemetz
10d ago
190 if err != nil {
191 return nil, err
192 }
b739915c
Keenan Nemetz
10d ago
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)
5d440e37
Keenan Nemetz
10d ago
199 }
b739915c
Keenan Nemetz
10d ago
200 return req, nil
5d440e37
Keenan Nemetz
10d ago
201 }
14b5c648
Keenan Nemetz
9d ago
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.
205 func (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 }