Documentation
¶
Overview ¶
Package sqlite implements the Wegweiser store on SQLite.
SQLite allows one writer at a time. Go's database/sql knows nothing about that and will happily hand two goroutines two connections to the same file, where they deadlock against each other: a deadlock the busy handler cannot resolve, because backing off does not release the lock the other connection is already holding. The result is SQLITE_BUSY under load with a busy timeout configured, which is the most common way to end up with a mysteriously flaky SQLite backend in Go.
- the write pool, capped at a single connection, which every Store.Update goes through, and which opens its transactions as BEGIN IMMEDIATE so that a lock conflict with another process surfaces at the start rather than halfway in;
- the read pool, which several readers share. Write-ahead logging lets them read while a write is in progress, and PRAGMA query_only makes a stray write through this pool an error instead of a violation of the single-writer assumption.
Index ¶
- Constants
- type Options
- type Store
- func (s *Store) Capabilities() store.Capabilities
- func (s *Store) Close() error
- func (r Store) CommitByID(ctx context.Context, cid journal.CommitID) (*journal.Commit, error)
- func (r Store) IterZoneRecords(ctx context.Context, zid zone.ZoneID) iter.Seq2[*zone.Record, error]
- func (r Store) IterZones(ctx context.Context) iter.Seq2[*zone.Zone, error]
- func (r Store) ListCommits(ctx context.Context, f store.CommitFilter) (store.Page[*journal.Commit], error)
- func (r Store) ListRecords(ctx context.Context, f store.RecordFilter) (store.Page[*zone.Record], error)
- func (r Store) ListTokens(ctx context.Context) (_ []*store.Token, err error)
- func (r Store) ListZones(ctx context.Context, f store.ZoneFilter) (store.Page[*zone.Zone], error)
- func (r Store) ManagedBy(ctx context.Context, rid zone.RecordID) ([]*zone.Record, error)
- func (r Store) ManagedByZone(ctx context.Context, zid zone.ZoneID) iter.Seq2[*zone.Record, error]
- func (s *Store) Migrate(ctx context.Context) error
- func (s *Store) Path() string
- func (s *Store) Ping(ctx context.Context) error
- func (r Store) RecordByID(ctx context.Context, rid zone.RecordID) (*zone.Record, error)
- func (r Store) RecordsByAddress(ctx context.Context, addr netip.Addr) ([]*zone.Record, error)
- func (r Store) ReverseZoneFor(ctx context.Context, addr netip.Addr) (*zone.Zone, error)
- func (r Store) Setting(ctx context.Context, key string) ([]byte, error)
- func (r Store) TokenByHash(ctx context.Context, hash []byte) (*store.Token, error)
- func (s *Store) Update(ctx context.Context, fn func(store.Tx) error) error
- func (s *Store) View(ctx context.Context, fn func(store.Reader) error) error
- func (r Store) ZoneByID(ctx context.Context, zid zone.ZoneID) (*zone.Zone, error)
- func (r Store) ZoneByName(ctx context.Context, name zone.Name) (*zone.Zone, error)
Constants ¶
const ( // DefaultBusyTimeout is how long a connection waits for a lock held by // another process before giving up. DefaultBusyTimeout = 5 * time.Second // MinReaders is the smallest read pool that still lets one reader proceed // while another is blocked. MinReaders = 2 )
Defaults for Options.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Options ¶
type Options struct {
// Path is the database file. It is created if it does not exist.
Path string
// MaxReaders is the size of the read pool. Zero picks a default based on
// the number of CPUs; anything below [MinReaders] is raised to it.
MaxReaders int
// BusyTimeout is how long to wait for a lock before failing. Zero picks
// [DefaultBusyTimeout].
BusyTimeout time.Duration
// Now supplies the current time for the timestamps the store owns. Nil
// picks [time.Now]. Tests set it to keep results reproducible.
Now func() time.Time
}
Options configure a SQLite store.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is a SQLite-backed store.Store.
It embeds the read side, so a read outside a transaction runs the same statement against the read pool that a read inside one runs against the transaction. The write side is deliberately not embedded: it lives on the transaction alone, so that no caller can write without one.
func Open ¶
Open opens the database at opts.Path, creating it if necessary, and verifies that both connection pools carry the settings they were configured with.
It does not apply migrations: a freshly opened store may be several schema versions behind, and whether to change the database on start is the caller's decision, not this package's. Call Store.Migrate for that.
func (*Store) Capabilities ¶
func (s *Store) Capabilities() store.Capabilities
Capabilities reports what this backend can do.
func (Store) CommitByID ¶
CommitByID returns one commit with its events.
func (Store) IterZoneRecords ¶
IterZoneRecords streams every record of a zone in canonical order.
func (Store) ListCommits ¶
func (r Store) ListCommits(ctx context.Context, f store.CommitFilter) (store.Page[*journal.Commit], error)
ListCommits returns one page of commit metadata, newest first.
func (Store) ListRecords ¶
func (r Store) ListRecords(ctx context.Context, f store.RecordFilter) (store.Page[*zone.Record], error)
ListRecords returns one page of records in canonical order.
func (Store) ListTokens ¶
ListTokens returns every token, revoked and expired ones included, so the UI can show a full history. Secrets are not stored and cannot be returned.
func (Store) ManagedByZone ¶
ManagedByZone streams the records generated from any record of a zone and living somewhere else.
func (*Store) Ping ¶
Ping reports whether the database is reachable. It checks both pools, since a reader failing while the writer works is exactly the asymmetry a health check exists to catch.
func (Store) RecordByID ¶
RecordByID returns one record.
func (Store) RecordsByAddress ¶
RecordsByAddress returns every A and AAAA record pointing at addr, across all zones.
func (Store) ReverseZoneFor ¶
ReverseZoneFor returns the most specific reverse zone covering addr.
The obvious formulation (ask for all 33 possible IPv4 networks, or all 129 IPv6 ones, in a single row-value IN list) measured badly twice over. SQLite answers it by scanning every zone rather than by using the index, and the query text itself grows to 129 branches that have to be parsed on every call. At 2000 zones that was around 0.5 ms for one lookup.
So it asks a smaller question first: which prefix lengths exist at all? A deployment has a handful, not 129, and the answer comes from a covering index without touching the table. Only those lengths are then looked up, in one indexed query, and it no longer matters whether the address is IPv4 or IPv6 — see BenchmarkReverseZoneFor, which measures roughly 45 microseconds at ten zones, 55 at a hundred and 240 at two thousand.
func (Store) TokenByHash ¶
TokenByHash resolves an API token by the SHA-256 of its secret.
The filtering happens in the query rather than after it, so that unknown, revoked and expired all leave by the same door: a caller comparing how long the three take, or which error comes back, learns nothing about which tokens exist.
func (*Store) View ¶
View runs fn inside one read transaction, so that several reads see one state. Write-ahead logging means it does not block the writer.