| 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. |
| 6 | package store |
| 7 | |
| 8 | import ( |
| 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. |
| 28 | const MaxCellsPerTab = 200_000 |
| 29 | |
| 30 | // ErrTabTooLarge is returned by Open when a tab's cell count exceeds |
| 31 | // MaxCellsPerTab. |
| 32 | var 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. |
| 38 | type 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. |
| 46 | type 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. |
| 83 | type handleKey struct { |
| 84 | db *gorm.DB |
| 85 | tabID uint64 |
| 86 | } |
| 87 | |
| 88 | var ( |
| 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. |
| 104 | func 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. |
| 138 | func (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. |
| 152 | func (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. |
| 168 | func (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). |
| 187 | func (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. |
| 205 | type handleEnv struct{ now time.Time } |
| 206 | |
| 207 | func newHandleEnv() handleEnv { return handleEnv{now: time.Now().UTC()} } |
| 208 | |
| 209 | func (e handleEnv) CellValue(calc.CellKey) calc.Value { return calc.Value{} } |
| 210 | func (e handleEnv) Now() time.Time { return e.now } |
| 211 | |