~bigbes/sr-ht-dolt

ref: bbaaa1f7b2e833698b0db403de50fc6f160c2bae sr-ht-dolt/beads/prefixes.go -rw-r--r-- 11.0 KiB
bbaaa1f7 — Eugene Blikh web: the milestones page says when its rollup is partial 5 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
package beads

import (
	"context"
	"regexp"
	"sort"
	"strings"
	"time"

	"sourcecraft.dev/bigbes/sr-ht-dolt/browse"
)

// --- which database owns "<prefix>-<id>" --------------------------------------
//
// A global-tracker issue that says "blocked by artifacts-nex.2" is naming a row
// in another database, and without this the reader has to work out which one and
// go there by hand. Two levels of tracker only pay for themselves if they point
// at each other.
//
// This file answers the one question that needs several databases: given an
// id-shaped string in some text, which database — of the ones this caller may
// browse — owns it. Whether the answer becomes a link, and what that link looks
// like, is the renderer's business; this package renders nothing.
//
// Authorization is not here, exactly as it is not in ReadyAcross: handing this
// function a database is the statement that the caller may read it. A database
// the caller may not browse is one the caller's index has never heard of, which
// is what makes an id belonging to it plain text and not a hint that it exists.

// prefixKey is the config row every beads database stores its own id prefix in
// ("global", "artifacts", "sr-ht-dolt", …). It sits in the same config table the
// memories do, beside compact_tier2_days and the rest of the tracker's settings.
const prefixKey = "issue_prefix"

// PrefixCache is the prefix index's projection cache: one prefix per database,
// gated on the head hash and expiring on ReadyCacheTTL — /ready's bounds and not
// a second set (see cache.go). The zero value is usable, so a holder that has no
// constructor can carry one as a field.
type PrefixCache struct {
	projectionCache[prefixEntry]
}

// prefixEntry is one database's cached prefix projection: the prefix its config
// names, and the total that read reported. The total is cached with the prefix
// rather than recomputed because a cache hit reads no row — it must say what
// the read that produced it said, including that the read was partial.
type prefixEntry struct {
	prefix string
	// configTotal is the config table's reported total, clipped or not. Over Max
	// it means the read stopped short, and an absent prefix is then a row that
	// was never reached rather than a row that does not exist.
	configTotal int
}

// PrefixIndex is prefix → the database that owns it, over the databases one
// caller may browse. It is built per caller and must not be shared between them:
// what it holds is precisely the set of databases that caller is allowed to know
// exists.
type PrefixIndex struct {
	byPrefix map[string]ReadyDatabase
	// Failed lists the databases that could not be read. It is here for the
	// caller's log and for nothing else: a database that could not be read costs
	// the ids it owns their link, and a page may not say more than that.
	Failed []ReadyFailure
	// Truncated lists the databases whose config table exceeded Max and came back
	// clipped, in the order the caller listed them. A clipped config is how a
	// tracker loses its prefix without failing: the issue_prefix row simply was
	// not among the rows read, so every id pointing at that tracker stops linking
	// and the index looks complete. It is a row clip, unrelated to the ceiling on
	// how many databases one build opens.
	Truncated []PrefixTruncation
}

// PrefixTruncation is one database whose config table came back clipped.
type PrefixTruncation struct {
	Database ReadyDatabase
	// ShownOf is that config table's reported total: rows that exist, against the
	// at most Max that were read.
	ShownOf int
}

// Reference is one id found in a text: where it sits, what it says, and which
// database owns it.
type Reference struct {
	Start, End int    // byte offsets of the id within the scanned text
	ID         string // the id exactly as written
	Database   ReadyDatabase
}

// idPattern matches an id-shaped token: a prefix of one or more lowercase
// alphanumeric segments joined by hyphens, then a hyphen, then the suffix bd
// generates — `[0-9a-z]+` with optional dotted parts, which is the `46c.2`
// subtask form.
//
// The suffix carries no hyphen, so the last hyphen of a token is always the
// prefix/suffix boundary and the split below is unambiguous even for a prefix
// that is itself hyphenated ("sr-ht-dolt-44n.6" is sr-ht-dolt's 44n.6, never
// sr-ht's dolt-44n.6). The word boundaries keep a token from being found inside
// a longer word, so "Xartifacts-46c" names nothing.
//
// The token being id-shaped is not what makes it an id: only a prefix the index
// knows makes it one. Everything else is left as it was written.
var idPattern = regexp.MustCompile(`\b[0-9a-z_]+(?:-[0-9a-z_]+)*-[0-9a-z]+(?:\.[0-9a-z]+)*\b`)

// PrefixesAcross builds the index over the databases it is handed: for each, the
// issue_prefix its config names.
//
// It is bounded exactly as ReadyAcross is — the same head-hash gate, the same
// TTL, the same ceiling — because it is the same read pattern: N stores opened
// for one request. It is cheaper per database, though: the projection is one row
// of a dozen-row table, and the fingerprint is not asked for separately, since a
// config table carrying issue_prefix is itself the statement that this is a bd
// tracker. A database with no such row simply owns no prefix, and that answer is
// cached like any other.
//
// now is the clock the TTL is measured against, passed in rather than read here
// for the reason ReadyAcross takes one: this package reads no hidden clock.
//
// It returns no error. A database that cannot be opened or read costs the ids it
// owns their link and nothing else; it lands in Failed for the caller's log.
func PrefixesAcross(
	ctx context.Context,
	dbs []ReadyDatabase,
	open ReadyOpener,
	cache *PrefixCache,
	now time.Time,
) *PrefixIndex {
	index := &PrefixIndex{byPrefix: map[string]ReadyDatabase{}}

	if len(dbs) > ReadyMaxDatabases {
		// The first Max in the order the caller listed them. The caller puts the
		// database whose page this is first, so the one prefix a page cannot do
		// without is the one the ceiling can never drop.
		dbs = dbs[:ReadyMaxDatabases]
	}

	// A prefix two databases both claim is dropped rather than awarded to
	// whichever was listed first: linking to one of two candidates would be a
	// guess rendered as a fact, and the id is readable as text either way.
	ambiguous := map[string]bool{}
	for _, d := range dbs {
		entry, err := databasePrefix(ctx, d, open, cache, now)
		if err != nil {
			index.Failed = append(index.Failed, ReadyFailure{Database: d, Err: err})
			continue
		}
		if entry.configTotal > Max {
			// Recorded before the empty-prefix skip below, because a clipped read is
			// the one case where an empty prefix is not an answer: the row may be
			// sitting in the tail this build never saw.
			index.Truncated = append(index.Truncated, PrefixTruncation{
				Database: d,
				ShownOf:  entry.configTotal,
			})
		}
		prefix := entry.prefix
		if prefix == "" {
			continue
		}
		if ambiguous[prefix] {
			continue
		}
		if _, taken := index.byPrefix[prefix]; taken {
			ambiguous[prefix] = true
			delete(index.byPrefix, prefix)
			continue
		}
		index.byPrefix[prefix] = d
	}
	return index
}

// databasePrefix returns one database's issue prefix and the total its config
// read reported, from the cache when the head has not moved and by reading
// config otherwise.
//
// The order is the whole point of the gate: open, list branches, and only then
// consult the cache. Opening a session and reading the branch list is cheap;
// reading rows is not, and on a hit no row is touched.
func databasePrefix(
	ctx context.Context,
	d ReadyDatabase,
	open ReadyOpener,
	cache *PrefixCache,
	now time.Time,
) (prefixEntry, error) {
	sess, err := open(ctx, d)
	if err != nil {
		return prefixEntry{}, err
	}
	defer sess.Close()

	branches, err := sess.Branches(ctx)
	if err != nil {
		return prefixEntry{}, err
	}
	ref := browse.DefaultBranch(branches)
	if ref == "" {
		// A store with no branches carries no config either: nothing to read and
		// nothing to cache.
		return prefixEntry{}, nil
	}
	head := headHashOf(branches, ref)
	if entry, ok := cache.lookup(d.ID, head, now); ok {
		return entry, nil
	}

	entry, err := readPrefix(ctx, sess, ref)
	if err != nil {
		return prefixEntry{}, err
	}
	cache.store(d.ID, head, now, entry)
	return entry, nil
}

// readPrefix reads the issue_prefix row out of a database's config table, and
// the total that read reported. A missing table — this is not a bd tracker — is
// no prefix, the treatment every optional table gets here.
//
// The total comes back with the prefix because the two answers this can produce
// are otherwise identical: a config with no issue_prefix row and a config whose
// issue_prefix row was left past Max both arrive here as no prefix at all.
func readPrefix(ctx context.Context, sess BrowseSession, ref string) (prefixEntry, error) {
	rows, total, err := readRowsOptional(ctx, sess, ref, memoryTable)
	if err != nil {
		return prefixEntry{}, err
	}
	entry := prefixEntry{configTotal: total}
	if rows == nil {
		return entry, nil
	}
	cols := indexCols(rows.Columns)
	for _, r := range rowsOf(rows) {
		if cell(cols, r, "key") != prefixKey {
			continue
		}
		// Lowercased because that is the case the ids themselves are written in,
		// and the index is looked up by what the text says.
		entry.prefix = strings.ToLower(strings.TrimSpace(cell(cols, r, "value")))
		return entry, nil
	}
	return entry, nil
}

// Lookup returns the database owning prefix. A nil index — no database was
// readable, or the caller did not build one for this page — knows no prefix,
// which is the same answer as an unknown one: not linked.
func (ix *PrefixIndex) Lookup(prefix string) (ReadyDatabase, bool) {
	if ix == nil {
		return ReadyDatabase{}, false
	}
	d, ok := ix.byPrefix[prefix]
	return d, ok
}

// Prefixes lists the prefixes the index knows, in order. It exists for the
// caller's log line and for tests.
func (ix *PrefixIndex) Prefixes() []string {
	if ix == nil {
		return nil
	}
	out := make([]string, 0, len(ix.byPrefix))
	for p := range ix.byPrefix {
		out = append(out, p)
	}
	sort.Strings(out)
	return out
}

// Scan returns every id in text whose prefix this index knows, in order and
// non-overlapping.
//
// It answers where the ids are and nothing about how they should be rendered:
// the caller escapes the text it was handed and wraps these ranges, which is the
// only order in which stored text can become HTML safely.
//
// An id-shaped token whose prefix the index does not know yields nothing at all,
// which is what makes an id in a database the caller may not browse
// indistinguishable from one that matches nothing.
func (ix *PrefixIndex) Scan(text string) []Reference {
	if ix == nil || len(ix.byPrefix) == 0 || text == "" {
		return nil
	}
	var out []Reference
	for _, m := range idPattern.FindAllStringIndex(text, -1) {
		token := text[m[0]:m[1]]
		cut := strings.LastIndex(token, "-")
		if cut <= 0 {
			continue
		}
		d, ok := ix.byPrefix[token[:cut]]
		if !ok {
			continue
		}
		out = append(out, Reference{Start: m[0], End: m[1], ID: token, Database: d})
	}
	return out
}