carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "errors"
5 "fmt"
6 "log"
7 "net/http"
8 "os"
9 "path/filepath"
10 "strings"
11 "sync"
12)
13
14// seedScratch is the console's one scratch reservation pool, shared by
15// --from restores and import spools (seeded-instances spec §4 step 5,
16// §5.2): an in-process counter over <console data>/scratch/seeds/, and the
17// per-app and deployment limits on how many run at once.
18type seedScratch struct {
19 root string
20 free func(string) (int64, error)
21
22 mu sync.Mutex
23 capacity int64
24 used int64
25 running int
26 perApp map[string]bool
27}
28
29const (
30 seedScratchSlack = 1 << 30 // capacity is free space less this
31 seedScratchAllApps = 2 // reservations across the deployment; one per app
32
33 // seedScratchPrefix starts the name of every entry the pool's users
34 // create directly inside the scratch directory. Start-up cleanup removes
35 // entries with this prefix and nothing else.
36 seedScratchPrefix = "from-"
37)
38
39func newSeedScratch(root string, free func(string) (int64, error)) *seedScratch {
40 p := &seedScratch{root: root, free: free, perApp: map[string]bool{}}
41 p.readCapacity()
42 return p
43}
44
45// startSeedScratch is what NewServer calls: it clears the pool's leftovers
46// and builds the pool. An empty directory, or one the cleanup refuses,
47// leaves a pool with no capacity, so every reservation is a 507.
48func startSeedScratch(dir string) *seedScratch {
49 usable := dir != ""
50 if usable {
51 if err := clearSeedScratch(dir); err != nil {
52 log.Printf("console: seed scratch %s: %v (the seed doors will refuse)", dir, err)
53 usable = false
54 }
55 }
56 return newSeedScratch(dir, func(d string) (int64, error) {
57 if !usable {
58 return 0, errors.New("no usable seed scratch directory")
59 }
60 return scratchFree(d)
61 })
62}
63
64func (p *seedScratch) dir() string { return p.root }
65
66// readCapacity is called with p.mu held, or before the pool is shared.
67func (p *seedScratch) readCapacity() {
68 n, err := p.free(p.root)
69 if err != nil {
70 p.capacity = -1 // unavailable: below zero, so even a zero-byte reservation is refused
71 return
72 }
73 p.capacity = max(n-seedScratchSlack, 0)
74}
75
76// reserve takes bytes for appKey (account/app), or answers why not. The
77// returned release is safe to call more than once.
78func (p *seedScratch) reserve(appKey string, bytes int64) (func(), *doorRefusal) {
79 p.mu.Lock()
80 defer p.mu.Unlock()
81 if p.perApp[appKey] {
82 app := appKey[strings.LastIndex(appKey, "/")+1:]
83 return nil, &doorRefusal{http.StatusTooManyRequests, fmt.Sprintf("another copy or upload is running for %s. Try again when it finishes.", app)}
84 }
85 if p.running >= seedScratchAllApps {
86 return nil, &doorRefusal{http.StatusTooManyRequests, "the console is already running two copies or uploads. Try again in a few minutes."}
87 }
88 if p.used == 0 {
89 p.readCapacity()
90 }
91 if bytes < 0 || bytes > p.capacity-p.used {
92 return nil, seedScratchFull()
93 }
94 p.used += bytes
95 p.running++
96 p.perApp[appKey] = true
97 var once sync.Once
98 return func() {
99 once.Do(func() {
100 p.mu.Lock()
101 defer p.mu.Unlock()
102 p.used -= bytes
103 p.running--
104 delete(p.perApp, appKey)
105 })
106 }, nil
107}
108
109// seedScratchFull is the 507 the pool answers, and what a door answers
110// when it cannot make its directory in the scratch.
111func seedScratchFull() *doorRefusal {
112 return &doorRefusal{http.StatusInsufficientStorage, "the console has no room for this right now. Try again later."}
113}
114
115// clearSeedScratch removes what a crashed restore or spool left at console
116// start (spec §4 "Cleanup", §5.2): the entries named seedScratchPrefix* that
117// sit directly inside dir. Anything else in dir stays, and dir itself is
118// never removed. A relative path or "/" is refused; an empty dir does
119// nothing. A symlink entry is removed as a link, never followed.
120func clearSeedScratch(dir string) error {
121 if dir == "" {
122 return nil
123 }
124 if !filepath.IsAbs(dir) || filepath.Clean(dir) == string(filepath.Separator) {
125 return fmt.Errorf("refusing to clear %q: it must be an absolute path other than /", dir)
126 }
127 dir = filepath.Clean(dir)
128 if err := os.MkdirAll(dir, 0o700); err != nil {
129 return err
130 }
131 // The directory holds copies of customer databases: narrow one that
132 // already existed with wider permissions.
133 if err := os.Chmod(dir, 0o700); err != nil {
134 return err
135 }
136 ents, err := os.ReadDir(dir)
137 if err != nil {
138 return err
139 }
140 var errs []error
141 for _, e := range ents {
142 if !strings.HasPrefix(e.Name(), seedScratchPrefix) {
143 continue
144 }
145 if err := os.RemoveAll(filepath.Join(dir, e.Name())); err != nil {
146 errs = append(errs, err)
147 }
148 }
149 return errors.Join(errs...)
150}
151