Documentation
¶
Overview ¶
Package hoppersqlite is hopper's SQLite driver, for an application that runs on one machine and wants hopper without a database server: a command-line tool that runs a workflow, a single-node service, local development. It is pure Go (modernc.org/sqlite, SQLite transpiled to Go), so a program that uses it builds with CGO_ENABLED=0.
d, err := hoppersqlite.Open("/var/lib/app/hopper.db", nil)
if _, err := hoppermigrate.Up(ctx, d, nil); err != nil { ... }
client, err := hopper.NewClient(d, cfg)
It passes the drivertest conformance suite, as the Postgres drivers do, and several processes may share one database. It uses internal packages of hopper's module, so it works with the hopper release of its own version: upgrade the two together. What SQLite's design makes different from Postgres:
- One transaction writes at a time (driver.Capabilities.SingleWriter). A transaction that writes holds the database's write lock until it ends, and every other write waits for it: within this process in turn, for as long as its context allows, and across processes through SQLite's busy handler, which polls rather than queues, so a process that writes without pause holds another's writes up for longer than its own. A claim is exclusive without SKIP LOCKED, and the transaction that finalizes a job settles its batch and promotes its dependents itself. A finalize with SkipLocked, or a progress write with NoWait, waits 100ms for a write lock held elsewhere, and then skips every job, or returns driver.ErrJobLocked.
- So a transaction held open holds up every client that shares the database, their lease renewals included: one held for longer than about the lease (hopper.Config.LeaseTTL) costs them their leases, and their running jobs are rescued. Keep transactions short.
- Transactions come from DB, whose connections take the write lock when a transaction runs its first statement, not when it begins, so that what it reads is never stale by the time it writes, and so that beginning one never blocks. A transaction begun read-only takes no lock and may not write. Within a transaction, write through it (InsertTx and the like), not through the client: a write on the pool would wait for the transaction's own lock. In a job's worker, whose context names the job's attempt, such a write fails at once with ErrLockHeldByOwner instead, if the worker began the transaction with that context or one derived from it; begin it so. Writes of the attempt with no transaction of its own open wait their turn, from however many goroutines.
- After some errors (an interrupted write, a full disk, an I/O error) SQLite rolls the whole transaction back itself. The transaction's later statements, and its Commit, then fail with ErrTransactionAborted, as those of a transaction an error aborted do on Postgres. An error that SQLite undoes only the statement of, such as a constraint's, leaves the transaction open, where Postgres would abort it. A COMMIT, END or ROLLBACK statement run in a transaction ends it: its later statements fail with sql.ErrTxDone. In a read-only transaction SQLite tells nothing of a COMMIT, and the statements after it, reads all, run on their own.
- The database must be on a local filesystem: Open refuses one on a network filesystem, or a virtual machine's shared folder, with ErrNetworkFilesystem, checking where symbolic links in the path lead. Its clock is therefore the host's, which every process that opens it shares, and the driver reads it as database time, never earlier than a time it read before in the same process (a host clock stepped back can still date one process's write before another's earlier one). A host that sleeps and wakes finds its clock moved on, as a database server's would, and may rescue the jobs of its own clients, whose leases expired while it slept.
- Commits are durable once they return (Config.Synchronous), on macOS with F_FULLFSYNC. The write-ahead log is cut back to 32MiB once a checkpoint empties it, and JobsMaintain truncates it once a reader that held up checkpoints is done.
- Notifications are rows of a table, delivered on commit to listeners in every process that opens the database: at once in this process, and within ListenInterval in others.
- History is one table, and retention deletes its rows in batches, without returning the space to the filesystem; JobsMaintain refreshes statistics but never vacuums. A stream's events commit in the order they are appended, so a position is an event's sequence number (Xid is always 0).
- Args, metadata and other JSON values are stored as given, not normalized as Postgres's jsonb normalizes them, and refused for what jsonb refuses. A metadata filter matches as jsonb's @> does.
- Job IDs are minted in Go, from crypto/rand, rather than by the database. There is no COPY: bulk inserts go through the one insert path. Nor are there SQL functions to insert jobs from SQL.
Index ¶
- Constants
- Variables
- func Migrations() []hoppermigrate.Migration
- type Config
- type Driver
- func (d *Driver) Capabilities() driver.Capabilities
- func (d *Driver) Close() error
- func (d *Driver) DB() *sql.DB
- func (d *Driver) Executor() driver.Executor
- func (d *Driver) Listener(ctx context.Context) (driver.Listener, error)
- func (d *Driver) Migrator() driver.Migrator
- func (d *Driver) UnwrapTx(tx *sql.Tx) driver.Executor
Constants ¶
const DefaultListenInterval = 50 * time.Millisecond
DefaultListenInterval is ListenInterval's default.
const DefaultOpenTimeout = 30 * time.Second
DefaultOpenTimeout is how long Open waits for the database: for another process that holds it while it creates it or recovers its log, say.
Variables ¶
var ErrLockHeldByOwner = errors.New("hoppersqlite: the write would wait for the database's write lock, which a transaction the same job attempt began holds open; write through that transaction, or end it first")
ErrLockHeldByOwner is returned for a write that would wait for the database's write lock while a transaction of the same owner holds it, one its caller began on DB and keeps open, which would wait until its context ended and hold every other write up meanwhile. The owner is a job's attempt (driver.WithOwner), which the worker's context carries: the check applies to a worker's transaction begun with that context, or one derived from it, and not to one begun with another. Write through that transaction instead, as InsertTx and the other Tx methods do, or end it first. Writes of one attempt that are not in a transaction of its own, from however many goroutines, wait their turn as any others do. A worker that writes from two goroutines at once, one of them in its own transaction, gets it, where Postgres would run both; give the other goroutine a context without the owner (driver.WithOwner(ctx, nil)) to have it wait its turn instead.
var ErrNetworkFilesystem = errors.New("hoppersqlite: the database is on a network filesystem, where SQLite cannot lock it reliably; choose a path on a local disk")
ErrNetworkFilesystem is returned by Open for a database on a network or cluster filesystem (NFS, SMB, Lustre, GPFS and the like, or FUSE, which such filesystems often are), where SQLite's file locks and the shared memory its write-ahead log needs do not work reliably: two processes could both believe they hold the write lock and corrupt the database. Home directories on clusters are often on one, and so are the folders a virtual machine or container shares with its host, Docker Desktop's bind mounts among them. Choose a path on a local disk instead.
var ErrTransactionAborted = errors.New("hoppersqlite: the transaction was rolled back after an error and runs no more statements; roll it back")
ErrTransactionAborted is returned for a statement of a transaction that SQLite rolled back itself after an error, such as an interrupted write, a full disk or an I/O error: the transaction's earlier statements are undone, and its later ones are refused rather than run on their own, as Postgres refuses those of a transaction an error aborted. Roll it back; Commit rolls it back and returns this error too.
Functions ¶
func Migrations ¶
func Migrations() []hoppermigrate.Migration
Migrations returns SQLite's migration scripts, one for every schema version hoppermigrate knows, in ascending order, for an application that applies its schema with a tool of its own rather than hoppermigrate.Up, whose Migrations returns Postgres's. Record each version applied in hopper_schema, as Up does.
Types ¶
type Config ¶
type Config struct {
// Synchronous is SQLite's synchronous setting for the database's
// connections: "FULL" (the default), which makes every commit durable
// before it returns, as Postgres's default does (on macOS with
// F_FULLFSYNC, without which the disk may hold the commit in its
// cache), or "NORMAL", with which a power failure, not a crash of the
// process, can lose the last commits before a checkpoint, and commits
// take less time.
Synchronous string
// ListenInterval is how often a listener looks for notifications
// committed by other processes. Those of this process wake it at once.
// Zero means 50ms.
ListenInterval time.Duration
// Logger receives the driver's warnings, such as its clock following
// the host's back after the host's clock was corrected. Nil means
// slog.Default().
Logger *slog.Logger
}
Config tunes the SQLite driver. A nil *Config means the defaults.
type Driver ¶
type Driver struct {
// contains filtered or unexported fields
}
Driver implements driver.Driver[*sql.Tx] on a SQLite database.
func Open ¶
Open opens the SQLite database at path as OpenContext does, waiting at most DefaultOpenTimeout.
func OpenContext ¶
OpenContext opens the SQLite database at path, creating it if needed, in write-ahead log mode, waiting for as long as ctx allows for another process that holds it. A symbolic link in the path is resolved, as SQLite resolves it, and the database opened where it leads. OpenContext returns ErrNetworkFilesystem for a database on a network filesystem. Close the Driver to close the database.
func (*Driver) Capabilities ¶
func (d *Driver) Capabilities() driver.Capabilities
Capabilities implements driver.Driver.
func (*Driver) DB ¶
DB returns the database/sql handle the driver uses. Begin the transactions you pass to InsertTx and the like on it: its connections take the database's write lock as a transaction's first statement runs. Leave its limit on open connections alone: each listener, and the migrator, holds a connection of its own for as long as it runs, and a limit that left a transaction without one would wait for them.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
hopperbench-sqlite
command
Command hopperbench-sqlite measures hopper's hot paths on SQLite, as hopperbench does on Postgres, and prints its results in hopperbench's format, one JSON object per scenario, which hopperbench -compare reads.
|
Command hopperbench-sqlite measures hopper's hot paths on SQLite, as hopperbench does on Postgres, and prints its results in hopperbench's format, one JSON object per scenario, which hopperbench -compare reads. |