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.
| 1 | package web |
| 2 | |
| 3 | // Bounds for the pages that shell out to git. |
| 4 | // |
| 5 | // Search, compare, blame, history and archives each start a git process against a |
| 6 | // repository somebody else controls. A repository can be large, a diff can be |
| 7 | // quadratic, and a blame walks history: none of these is bounded by anything the |
| 8 | // instance configured, and all of them are reachable by an anonymous reader on a |
| 9 | // public repository. So each one takes a slot from a fixed-size pool and runs under |
| 10 | // a deadline; past the wait the reader is told to come back rather than queued |
| 11 | // behind an unbounded number of other readers. |
| 12 | |
| 13 | import ( |
| 14 | "context" |
| 15 | "net/http" |
| 16 | "time" |
| 17 | |
| 18 | "rodent.software/vole/core/git" |
| 19 | "rodent.software/vole/forge/repo" |
| 20 | ) |
| 21 | |
| 22 | const ( |
| 23 | // webGitConcurrent bounds how many of these pages run git at once. |
| 24 | webGitConcurrent = 8 |
| 25 | // webGitTimeout bounds one page's git work. A read of a repository this size |
| 26 | // takes milliseconds; a minute means something is wrong, and the process is |
| 27 | // killed rather than left to finish after the reader has gone. |
| 28 | webGitTimeout = 30 * time.Second |
| 29 | // webGitWait is how long a page waits for a slot before answering 503. |
| 30 | webGitWait = 5 * time.Second |
| 31 | ) |
| 32 | |
| 33 | // gitSlot takes one of the bounded slots, waiting a short while so a burst of page |
| 34 | // loads queues instead of failing. It answers the request itself when there is no |
| 35 | // slot, and reports whether the handler should continue. |
| 36 | func (w *Web) gitSlot(rw http.ResponseWriter, r *http.Request) (func(), bool) { |
| 37 | if w.gitSem == nil { |
| 38 | return func() {}, true |
| 39 | } |
| 40 | select { |
| 41 | case w.gitSem <- struct{}{}: |
| 42 | return func() { <-w.gitSem }, true |
| 43 | default: |
| 44 | } |
| 45 | wait := w.gitWait |
| 46 | if wait <= 0 { |
| 47 | wait = webGitWait |
| 48 | } |
| 49 | t := time.NewTimer(wait) |
| 50 | defer t.Stop() |
| 51 | select { |
| 52 | case w.gitSem <- struct{}{}: |
| 53 | return func() { <-w.gitSem }, true |
| 54 | case <-t.C: |
| 55 | rw.Header().Set("Retry-After", "5") |
| 56 | http.Error(rw, "the server is busy running other repository queries; try again shortly", http.StatusServiceUnavailable) |
| 57 | return nil, false |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | // gitRepo returns the repository handle with this page's deadline attached, so a |
| 62 | // git process that outlives it is killed instead of holding a slot. The caller must |
| 63 | // call cancel when the handler is done. |
| 64 | func gitRepo(r *http.Request, m *repo.Meta) (*git.Repo, context.CancelFunc) { |
| 65 | ctx, cancel := context.WithTimeout(r.Context(), webGitTimeout) |
| 66 | return m.Git().WithContext(ctx), cancel |
| 67 | } |
| 68 |