oficina / sheets Public

Clone
git clone https://amadan.net/oficina/sheets

Plain git — no account needed to clone.

Download

Download this file

1// Package store's handle.go is the in-memory sheet handle: opening a tab
2// decrypts it into a refcounted graph, serves reads from that graph, and
3// recovers from a crash mid-recalculation before it serves a single one.
4// See handle_open.go for the load-and-recover path and handle_set.go for
5// the write path.
6package store
7
8import (
9 "errors"
10 "fmt"
11 "sync"
12 "time"
13
14 "gorm.io/gorm"
15
16 "amadan.net/oficina/sheets/internal/sheets/calc"
17 "amadan.net/oficina/sheets/internal/sheets/seal"
18)
19
20// MaxCellsPerTab is a hard cap.
21//
22// The handle holds a decrypted tab plus its dependency graph in memory, so
23// "hold it in RAM" is only true up to a number. 200,000 keeps a tab in tens
24// of megabytes on a small shared box where many instances live. Google's
25// limit is 10M and essentially nobody reaches it, so this trades a limit
26// almost nobody meets for a memory ceiling everybody benefits from. Past it,
27// an honest error rather than a slow death.
28const MaxCellsPerTab = 200_000
29
30// ErrTabTooLarge is returned by Open when a tab's cell count exceeds
31// MaxCellsPerTab.
32var ErrTabTooLarge = errors.New("store: tab exceeds MaxCellsPerTab")
33
34// cellIDs is a cell's real, stable database identity — as opposed to the
35// position-derived calc.CellKey the graph and every formula reference
36// inside it are keyed by. See handle_open.go's doc comment on the binder
37// for why the two are not the same numbers.
38type cellIDs struct{ RowID, ColID uint64 }
39
40// Handle is one tab, decrypted into memory: its dependency graph, its
41// cells' raw input text, and the row/col position bindings that translate
42// between a cell's real (row_id, col_id) and the graph's key space. A
43// Handle is not safe for concurrent use from multiple goroutines without
44// its own lock — which is exactly what mu provides; every exported method
45// takes it.
46type Handle struct {
47 mu sync.Mutex
48 db *gorm.DB
49 sealer *seal.Sealer
50 tabID uint64
51 graph *calc.Graph
52
53 rowPos map[uint64]int // real Row.ID -> Position
54 colPos map[uint64]int // real Col.ID -> Position
55
56 // rowByPos and colByPos are rowPos/colPos inverted — Position ->
57 // real ID — kept in lockstep with them (buildHandle populates both
58 // pairs together; EnsureRow/EnsureCol, handle_grid.go, are the only
59 // other writers). Row/Col records are never deleted or moved by
60 // anything in this package (setBlank deletes a Cell row, never a
61 // Row or Col — see handle_set.go), so a lookup here never goes
62 // stale within a handle's lifetime. They exist for callers, like
63 // xlsx import, that address cells by spreadsheet position (A1
64 // notation) rather than by stable id and need an O(1) "does a row
65 // already exist at position N" check instead of a linear scan.
66 rowByPos map[int]uint64
67 colByPos map[int]uint64
68
69 rawText map[calc.CellKey]string // decrypted input text; in-memory only, never persisted as-is
70 keyToIDs map[calc.CellKey]cellIDs // graph key -> the real ids that own it
71
72 nextVersion uint64 // next input/cache-version stamp this handle will hand out
73
74 regKey handleKey
75 refCount int
76}
77
78// handleKey scopes the process-wide handle registry to a specific
79// database as well as a tab id — otherwise two tests (or, in principle,
80// two differently-configured stores in the same process) opening "tab 1"
81// against different *gorm.DB values would collide on the same cached
82// handle.
83type handleKey struct {
84 db *gorm.DB
85 tabID uint64
86}
87
88var (
89 registryMu sync.Mutex
90 registry = map[handleKey]*Handle{}
91)
92
93// Open returns the refcounted handle for tabID, building it from db if no
94// live handle exists yet, or incrementing the refcount and returning the
95// existing one if it does. Every Open must be matched with a Release.
96//
97// Open counts the tab's cells before doing anything else — refusing a
98// tab past MaxCellsPerTab is the one check cheap enough to run unconditionally,
99// and it must run before any decrypt work, or the cap protects nothing.
100// If the tab is under the cap, Open decrypts it, builds the dependency
101// graph, and recalculates forward from any cell whose CacheVersion trails
102// its InputVersion — see handle_open.go — before this call returns, so no
103// caller of Value ever observes a cache a crash left behind.
104func Open(db *gorm.DB, sealer *seal.Sealer, tabID uint64) (*Handle, error) {
105 var count int64
106 if err := db.Model(&Cell{}).Where("tab_id = ?", tabID).Count(&count).Error; err != nil {
107 return nil, fmt.Errorf("store: count cells for tab: %w", err)
108 }
109 if count > MaxCellsPerTab {
110 return nil, ErrTabTooLarge
111 }
112
113 key := handleKey{db: db, tabID: tabID}
114
115 registryMu.Lock()
116 defer registryMu.Unlock()
117
118 if h, ok := registry[key]; ok {
119 h.refCount++
120 return h, nil
121 }
122
123 h, err := buildHandle(db, sealer, tabID)
124 if err != nil {
125 return nil, err
126 }
127 h.regKey = key
128 h.refCount = 1
129 registry[key] = h
130 return h, nil
131}
132
133// Release decrements the handle's refcount. At zero, the handle is idle
134// and is evicted immediately: the next Open for this tab rebuilds it from
135// the database rather than resuming a stale in-memory copy, which is what
136// lets a process restart (or, in a test, a deliberate simulated crash
137// between Release and the next Open) be observed truthfully.
138func (h *Handle) Release() {
139 registryMu.Lock()
140 defer registryMu.Unlock()
141 h.refCount--
142 if h.refCount <= 0 {
143 delete(registry, h.regKey)
144 }
145}
146
147// Value returns key's current value: whatever the graph has cached for
148// it, or an empty Value for a cell with no content (never set, blanked,
149// or referencing a row/col this tab doesn't have). It never evaluates —
150// Open and Set are what keep the graph's cache current, so a read is
151// always just a lookup.
152func (h *Handle) Value(rowID, colID uint64) calc.Value {
153 h.mu.Lock()
154 defer h.mu.Unlock()
155 key, ok := h.cellKey(rowID, colID)
156 if !ok {
157 return calc.Value{}
158 }
159 v, _ := h.graph.CachedValue(key)
160 return v
161}
162
163// Format returns the interned Format a cell uses, or nil if the cell has
164// no formatting (or does not exist). Unlike Value, this reads straight
165// through to the database rather than through the in-memory graph: format
166// assignment is comparatively rare and Format rows are already interned
167// (models.go), so there is no equivalent cache to keep warm here.
168func (h *Handle) Format(rowID, colID uint64) *Format {
169 var cell Cell
170 if err := h.db.Where("tab_id = ? AND row_id = ? AND col_id = ?", h.tabID, rowID, colID).
171 First(&cell).Error; err != nil {
172 return nil
173 }
174 if cell.FormatID == nil {
175 return nil
176 }
177 var f Format
178 if err := h.db.First(&f, *cell.FormatID).Error; err != nil {
179 return nil
180 }
181 return &f
182}
183
184// cellKey translates a cell's real, stable (row_id, col_id) into the
185// graph's position-derived CellKey space. false means rowID or colID does
186// not belong to this tab (or this handle has not loaded it).
187func (h *Handle) cellKey(rowID, colID uint64) (calc.CellKey, bool) {
188 rp, ok := h.rowPos[rowID]
189 if !ok {
190 return calc.CellKey{}, false
191 }
192 cp, ok := h.colPos[colID]
193 if !ok {
194 return calc.CellKey{}, false
195 }
196 return calc.CellKey{RowID: uint64(rp + 1), ColID: uint64(cp + 1)}, true
197}
198
199// handleEnv is calc.Env pinned to one instant for the lifetime of a
200// single Recalc pass — "one pass sees one instant" (calc.Env's own
201// doc) — and otherwise answering empty for any cell the graph does not
202// itself intercept, since every cell this handle tracks with content is
203// registered in the graph (see contentNode); a CellValue call reaching
204// this fallback means the cell has none.
205type handleEnv struct{ now time.Time }
206
207func newHandleEnv() handleEnv { return handleEnv{now: time.Now().UTC()} }
208
209func (e handleEnv) CellValue(calc.CellKey) calc.Value { return calc.Value{} }
210func (e handleEnv) Now() time.Time { return e.now }
211