ci: run CI on Buildkite (#4274)

Move atuin's CI over to Buildkite. I've removed a few jobs from branches
and pushed them onto main, mostly things like the nixos build, illumos
etc - they were really slow, and difficult to optimise.

The goal is to be on buildkit, and have CI take around a minute per
commit. Once we have more tests, we can keep parallelizing

I tested out both the nvme disks and the buildkite cache. The former is
much faster, but is trickier to isolate across branches. I would prefer
that there is 0 chance of a branch messing with a build on main, or with
another branch.

---------

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Ellie Huxtable
2026-09-26 17:45:57 -07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent d2ddf8c0f9
commit c7f0318eb3
17 changed files with 1127 additions and 32 deletions
+88
View File
@@ -0,0 +1,88 @@
# Buildkite Cache definitions, used by `buildkite-agent cache restore/save`
# in .buildkite/pipeline.yml.
#
# Keys go coarse to fine. The toolchain file is the last required part
# (fallback_limit), so a Cargo.lock change restores the newest entry for the
# same toolchain and cargo rebuilds only the dependencies that changed.
#
# Branch isolation comes from the cluster's cache registry policy: entries
# are saved scoped to their branch, and restores search the current branch
# and then main, so a branch build can never write into what main restores.
#
# Bump `v1` to invalidate a cache; an entry is written once per address.
caches:
# The crates.io index and downloaded .crate files. Identical for every
# step, so only the check step saves it.
- name: cargo_registry
cache_key:
- cargo-registry
- v1
- agent: os
- agent: arch
fallback_limit: true
- checksum: Cargo.lock
target_paths:
- ~/.cargo/registry/index
- ~/.cargo/registry/cache
# target/ after prune-target.sh: third-party dependency builds only. One
# entry per step, since each step builds a different profile and feature
# set (the key's `step` part is the step key).
- name: target
cache_key:
- target
# v2: dev/test builds without debuginfo (pipeline env).
- v2
- agent: step
- agent: os
- agent: arch
- checksum: rust-toolchain.toml
fallback_limit: true
- checksum: Cargo.lock
target_paths:
- target
# The step's Rust toolchain (rustup's dir plus ~/.cargo/bin, where rustup
# and its proxies live): restoring it takes a second or two against ~11s
# to download. Per step, since steps pin different toolchains (MSRV) and
# components (clippy). Linux only: macOS images ship several toolchains,
# so there the entry is 1.7 GB and slower to restore than installing. Not
# used for the nightly rustfmt step, which should pick up new nightlies.
- name: rustup
cache_key:
- rustup
- v1
- agent: step
- agent: os
- agent: arch
- checksum: rust-toolchain.toml
target_paths:
- ~/.rustup
- ~/.cargo/bin
# .debs apt_install downloaded (libssl-dev, pkg-config, and per-step extras
# like zsh/fish), installed with dpkg on later jobs. Per step because the
# package lists differ. Bump v1 if the hosted image's base packages move
# on (dpkg --refuse-downgrade falls back to apt meanwhile).
- name: apt_debs
cache_key:
- apt-debs
- v1
- agent: step
- agent: os
- agent: arch
target_paths:
- ~/.cache/apt-debs
# The OmniOS VM image anyvm downloads for the illumos step. Keyed on the
# script, which pins the anyvm, OmniOS and builder versions.
- name: anyvm
cache_key:
- anyvm
- v1
- agent: os
- agent: arch
- checksum: .buildkite/scripts/illumos.sh
target_paths:
- ~/.cache/anyvm
+516
View File
@@ -0,0 +1,516 @@
# atuin CI on Buildkite: PROTOTYPE, running alongside GitHub Actions to
# compare. Ports rust.yml (Linux + macOS; Windows and the illumos VM job
# stay on GitHub Actions for now), nix.yml, codespell.yml, shellcheck.yml,
# vale.yml, installer.yml and nightly.yml, with the same jobs and commands.
#
# Each step sources .buildkite/scripts/toolchain.sh (rustup + pinned tools,
# and the `section` log-group helper). Caching is Buildkite Cache
# (.buildkite/cache.yml): the cargo registry, plus a per-step target/ pruned
# to third-party dependency builds (prune-target.sh), keyed on the toolchain
# and Cargo.lock. Saves run only after the step's commands have succeeded.
#
# Queues: light jobs use the cluster's default queue (linux-small, 2 vCPU).
# Compile-bound jobs use linux-large (8 vCPU) and macOS jobs macos-large
# (12 vCPU), both default queues in every Buildkite cluster. Test steps are
# split across jobs with `parallelism` + nextest --partition.
#
# Not ported yet: the `paths-ignore: ui/**` filters on rust.yml and nix.yml
# (if_changed needs a diff base set for main builds first).
#
# Scheduled builds (the nightly schedule, set in the pipeline settings) run
# everything plus the nightly tests, which also keeps main's cache entries
# from expiring.
env:
CARGO_TERM_COLOR: always
# No debuginfo in dev/test builds: a big share of their compile and link
# time, unused in CI. debug_assertions stay on; panics still report
# file:line, backtraces just lose line numbers.
CARGO_PROFILE_DEV_DEBUG: "0"
CARGO_PROFILE_TEST_DEBUG: "0"
# .config/nextest.toml's ci profile: writes test-results/junit.xml, which
# the test-collector plugin uploads to Buildkite Test Engine.
NEXTEST_PROFILE: ci
steps:
- label: ":rust: Build (release)"
key: build
# main only: PRs are type-checked in the release profile by `check`
# (cargo check --release), and nearly all of this job is LLVM codegen
# for the atuin binary (~2 min on any machine).
if: build.branch == pipeline.default_branch
notify:
- github_check:
name: "buildkite/build"
agents:
queue: linux-large
timeout_in_minutes: 45
env:
# Only proves the release profile compiles (release.yml ships the
# `dist` profile), so skip LTO and spread codegen across more units.
CARGO_PROFILE_RELEASE_LTO: "off"
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: "256"
command:
- buildkite-agent cache restore --name cargo_registry --name target --name rustup --name apt_debs
- source .buildkite/scripts/toolchain.sh
- buildkite-agent cache save --name rustup --name apt_debs
- 'section ":hammer: cargo build --release" cargo build --workspace --locked --release'
- 'section ":broom: Prune target/" .buildkite/scripts/prune-target.sh'
- buildkite-agent cache save --name target
- label: ":test_tube: Unit tests (Linux) %n"
key: unit-test
notify:
- github_check:
name: "buildkite/unit-test"
agents:
queue: linux-large
# Split with nextest --partition; raise this as the suite grows.
parallelism: 2
timeout_in_minutes: 45
env:
CARGO_TOOLS: nextest
secrets:
- ATUIN_TEST_ENGINE_TOKEN
plugins: &test_collector
- test-collector#v1.12.0:
files: test-results/junit.xml
format: junit
api-token-env-name: ATUIN_TEST_ENGINE_TOKEN
command:
- buildkite-agent cache restore --name cargo_registry --name target --name rustup --name apt_debs
- source .buildkite/scripts/toolchain.sh
- buildkite-agent cache save --name rustup --name apt_debs
- 'section ":test_tube: cargo nextest (lib + bins)" cargo nextest run $$(nextest_partition) --lib --bins || { .buildkite/scripts/proptest-regressions.sh; exit 1; }'
- 'section ":broom: Prune target/" .buildkite/scripts/prune-target.sh'
- buildkite-agent cache save --name target
- label: ":test_tube: Unit tests (macOS) %n"
key: unit-test-macos
notify:
- github_check:
name: "buildkite/unit-test-macos"
agents:
queue: macos-large
# Split with nextest --partition; raise this as the suite grows.
parallelism: 2
timeout_in_minutes: 45
env:
CARGO_TOOLS: nextest
secrets:
- ATUIN_TEST_ENGINE_TOKEN
plugins: *test_collector
command:
- buildkite-agent cache restore --name cargo_registry --name target
- source .buildkite/scripts/toolchain.sh
# No rustup cache on macOS: the image ships several toolchains, so the
# entry is 1.7 GB (20s to restore) against ~4s to install 1.98.0.
- 'section ":test_tube: cargo nextest (lib + bins)" cargo nextest run $$(nextest_partition) --lib --bins || { .buildkite/scripts/proptest-regressions.sh; exit 1; }'
- 'section ":broom: Prune target/" .buildkite/scripts/prune-target.sh'
- buildkite-agent cache save --name target
- label: ":mag: Check (Linux): all features, no features, release"
key: check
notify:
- github_check:
name: "buildkite/check"
timeout_in_minutes: 45
command:
- buildkite-agent cache restore --name cargo_registry --name target --name rustup --name apt_debs
- source .buildkite/scripts/toolchain.sh
- buildkite-agent cache save --name rustup --name apt_debs
- 'section ":package: cargo fetch" cargo fetch --locked'
# Registry contents are the same for every step; this one saves them.
- buildkite-agent cache save --name cargo_registry
# --all-features enables vendored-tls, which compiles OpenSSL from
# source; type-checking can use the system libssl instead.
- 'section ":mag: check (all features)" env OPENSSL_NO_VENDOR=1 cargo check --all-features --workspace'
- 'section ":mag: check (no features)" cargo check --no-default-features --workspace'
- 'section ":mag: check (release profile)" cargo check --workspace --locked --release'
- 'section ":broom: Prune target/" .buildkite/scripts/prune-target.sh'
- buildkite-agent cache save --name target
# The rest of rust.yml's check matrix, in parallel with the step above.
- label: ":mag: Check (Linux): sync, server, client only"
key: check-subsets
notify:
- github_check:
name: "buildkite/check-subsets"
timeout_in_minutes: 45
command:
- buildkite-agent cache restore --name cargo_registry --name target --name rustup --name apt_debs
- source .buildkite/scripts/toolchain.sh
- buildkite-agent cache save --name rustup --name apt_debs
- 'section ":mag: check (sync)" cargo check --no-default-features --features sync --workspace'
- 'section ":mag: check (server)" cargo check -p atuin-server'
- 'section ":mag: check (client only)" cargo check --no-default-features --features client --workspace'
- 'section ":broom: Prune target/" .buildkite/scripts/prune-target.sh'
- buildkite-agent cache save --name target
- label: ":mag: Check (macOS)"
key: check-macos
notify:
- github_check:
name: "buildkite/check-macos"
agents:
queue: macos-large
timeout_in_minutes: 45
command:
- buildkite-agent cache restore --name cargo_registry --name target
- source .buildkite/scripts/toolchain.sh
# No rustup cache on macOS: the image ships several toolchains, so the
# entry is 1.7 GB (20s to restore) against ~4s to install 1.98.0.
- 'section ":package: cargo fetch" cargo fetch --locked'
# The registry key includes the OS, so macOS saves its own.
- buildkite-agent cache save --name cargo_registry
# No system libssl on macOS: --all-features builds vendored OpenSSL,
# as it does on GitHub Actions.
- 'section ":mag: check (all features)" cargo check --all-features --workspace'
- 'section ":mag: check (no features)" cargo check --no-default-features --workspace'
- 'section ":mag: check (sync)" cargo check --no-default-features --features sync --workspace'
- 'section ":mag: check (server)" cargo check -p atuin-server'
- 'section ":mag: check (client only)" cargo check --no-default-features --features client --workspace'
- 'section ":mag: check (release profile)" cargo check --workspace --locked --release'
- 'section ":broom: Prune target/" .buildkite/scripts/prune-target.sh'
- buildkite-agent cache save --name target
- label: ":crab: MSRV (1.95.0)"
key: msrv
notify:
- github_check:
name: "buildkite/msrv"
timeout_in_minutes: 45
env:
# rust-toolchain.toml pins the dev toolchain; override it explicitly.
RUST_TOOLCHAIN: "1.95.0"
command:
- buildkite-agent cache restore --name cargo_registry --name target --name rustup --name apt_debs
- source .buildkite/scripts/toolchain.sh
- buildkite-agent cache save --name rustup --name apt_debs
- 'section ":crab: check on MSRV (all features)" cargo check --locked --workspace --all-features'
- 'section ":broom: Prune target/" .buildkite/scripts/prune-target.sh'
- buildkite-agent cache save --name target
- label: ":postgres: Integration tests %n"
key: integration-test
notify:
- github_check:
name: "buildkite/integration-test"
agents:
queue: linux-large
# Split with nextest --partition; raise this as the suite grows.
parallelism: 2
timeout_in_minutes: 45
env:
CARGO_TOOLS: nextest
ATUIN_DB_URI: postgres://atuin:pass@localhost:5432/atuin
secrets:
- ATUIN_TEST_ENGINE_TOKEN
plugins: *test_collector
command:
- .buildkite/scripts/start-postgres.sh
- buildkite-agent cache restore --name cargo_registry --name target --name rustup --name apt_debs
- source .buildkite/scripts/toolchain.sh
- buildkite-agent cache save --name rustup --name apt_debs
# e2e tests have their own step. Test databases live in tempdirs;
# tmpfs keeps concurrent SQLite WAL fsyncs from stalling the disk.
- 'section ":test_tube: cargo nextest (integration)" env TMPDIR=/dev/shm cargo nextest run $$(nextest_partition) --test "*" -E "not binary(/e2e_.*/)" || { .buildkite/scripts/proptest-regressions.sh; exit 1; }'
- 'section ":broom: Prune target/" .buildkite/scripts/prune-target.sh'
- buildkite-agent cache save --name target
- label: ":terminal: e2e (Linux) %n"
key: e2e
notify:
- github_check:
name: "buildkite/e2e"
# Split with nextest --partition; raise this as the suite grows.
parallelism: 2
agents:
queue: linux-large
timeout_in_minutes: 45
env:
CARGO_TOOLS: nextest
APT_PACKAGES: "zsh fish xz-utils"
ATUIN_E2E_REQUIRE_SHELLS: "1"
secrets:
- ATUIN_TEST_ENGINE_TOKEN
plugins: *test_collector
command:
- buildkite-agent cache restore --name cargo_registry --name target --name rustup --name apt_debs
- source .buildkite/scripts/toolchain.sh
- buildkite-agent cache save --name rustup --name apt_debs
- 'section ":shell: Install ble.sh" .buildkite/scripts/install-blesh.sh'
- 'section ":terminal: cargo nextest (e2e)" cargo nextest run $$(nextest_partition) -p atuin --test "e2e_*"'
- 'section ":broom: Prune target/" .buildkite/scripts/prune-target.sh'
- buildkite-agent cache save --name target
- label: ":terminal: e2e (macOS)"
key: e2e-macos
notify:
- github_check:
name: "buildkite/e2e-macos"
agents:
queue: macos-large
timeout_in_minutes: 45
env:
CARGO_TOOLS: nextest
BREW_PACKAGES: "bash fish"
ATUIN_E2E_REQUIRE_SHELLS: "1"
secrets:
- ATUIN_TEST_ENGINE_TOKEN
plugins: *test_collector
command:
- buildkite-agent cache restore --name cargo_registry --name target
- source .buildkite/scripts/toolchain.sh
# No rustup cache on macOS: the image ships several toolchains, so the
# entry is 1.7 GB (20s to restore) against ~4s to install 1.98.0.
# Test with Homebrew's bash, not macOS's /bin/bash 3.2.
# ($$ escapes pipeline-upload interpolation.)
- export ATUIN_E2E_BASH="$$(brew --prefix bash)/bin/bash"
- 'section ":shell: Install ble.sh" .buildkite/scripts/install-blesh.sh'
- 'section ":terminal: cargo nextest (e2e)" cargo nextest run -p atuin --test "e2e_*"'
- 'section ":broom: Prune target/" .buildkite/scripts/prune-target.sh'
- buildkite-agent cache save --name target
- label: ":paperclip: Clippy"
key: clippy
notify:
- github_check:
name: "buildkite/clippy"
timeout_in_minutes: 45
env:
RUST_COMPONENTS: clippy
command:
- buildkite-agent cache restore --name cargo_registry --name target --name rustup --name apt_debs
- source .buildkite/scripts/toolchain.sh
- buildkite-agent cache save --name rustup --name apt_debs
# One run over the union of what `cargo clippy` (lib + bins) and
# `cargo clippy --tests` lint, so the two share their work.
- 'section ":paperclip: clippy (lib, bins, tests)" cargo clippy --lib --bins --tests -- -D warnings'
- 'section ":broom: Prune target/" .buildkite/scripts/prune-target.sh'
- buildkite-agent cache save --name target
- label: ":books: Docs"
key: docs
notify:
- github_check:
name: "buildkite/docs"
timeout_in_minutes: 45
env:
RUSTDOCFLAGS: "-D warnings"
command:
- buildkite-agent cache restore --name cargo_registry --name target --name rustup --name apt_debs
- source .buildkite/scripts/toolchain.sh
- buildkite-agent cache save --name rustup --name apt_debs
- 'section ":books: cargo doc" cargo doc --document-private-items --no-deps --workspace'
- 'section ":broom: Prune target/" .buildkite/scripts/prune-target.sh'
- buildkite-agent cache save --name target
- label: ":art: Format"
key: format
notify:
- github_check:
name: "buildkite/format"
timeout_in_minutes: 15
env:
RUST_TOOLCHAIN: nightly
RUST_COMPONENTS: rustfmt
command:
- source .buildkite/scripts/toolchain.sh
- 'section ":art: cargo fmt --check" cargo fmt -- --check'
- label: ":no_entry: cargo-deny"
key: deny
notify:
- github_check:
name: "buildkite/deny"
timeout_in_minutes: 15
env:
CARGO_TOOLS: deny
command:
- buildkite-agent cache restore --name cargo_registry --name rustup --name apt_debs
- source .buildkite/scripts/toolchain.sh
- buildkite-agent cache save --name rustup --name apt_debs
# Same arguments cargo-deny-action passes by default.
- 'section ":no_entry: cargo deny check" cargo deny --log-level warn --all-features check'
# Library crates must never (re)add eyre or anyhow; each has its own
# crate-scoped deny.toml.
- 'section ":no_entry: bans: atuin-common" cargo deny --log-level warn --manifest-path crates/atuin-common/Cargo.toml --all-features check bans'
- 'section ":no_entry: bans: atuin-history" cargo deny --log-level warn --manifest-path crates/atuin-history/Cargo.toml --all-features check bans'
- 'section ":no_entry: bans: atuin-domain" cargo deny --log-level warn --manifest-path crates/atuin-domain/Cargo.toml --all-features check bans'
- 'section ":no_entry: bans: atuin-pty-proxy" cargo deny --log-level warn --manifest-path crates/atuin-pty-proxy/Cargo.toml --all-features check bans'
- label: ":package: Package"
key: package
# main only: this only guards crates.io publishing (every crate builds
# from its packaged tarball), which isn't part of the PR flow. On main
# it still flags publishing breakage well before a release.
if: build.branch == pipeline.default_branch
notify:
- github_check:
name: "buildkite/package"
agents:
queue: linux-large
timeout_in_minutes: 45
command:
- buildkite-agent cache restore --name cargo_registry --name target --name rustup --name apt_debs
- source .buildkite/scripts/toolchain.sh
- buildkite-agent cache save --name rustup --name apt_debs
# Make sure the packaged crates build from their tarballs, not from
# anything left over from the workspace build. The restored target/
# holds only third-party builds (prune-target.sh), which verification
# reuses: it builds into the shared target/debug.
- rm -rf ~/.cargo/registry/{cache,src}/*/atuin* target/package
- 'section ":package: cargo package" cargo package --workspace'
- 'section ":broom: Prune target/" .buildkite/scripts/prune-target.sh'
- buildkite-agent cache save --name target
# rust.yml's "Test in VM (illumos)": builds the client and server on a real
# OmniOS VM (KVM-accelerated; hosted agents expose /dev/kvm). main only,
# like build and nix-build: it was ~12 min per PR on GitHub Actions.
- label: ":sunrise: Build on illumos (OmniOS VM)"
key: illumos
if: build.branch == pipeline.default_branch
notify:
- github_check:
name: "buildkite/illumos"
agents:
queue: linux-large
timeout_in_minutes: 45
command:
- buildkite-agent cache restore --name anyvm --name apt_debs
- .buildkite/scripts/illumos.sh
- buildkite-agent cache save --name anyvm --name apt_debs
- label: ":moon: Nightly tests"
key: nightly
if: build.source == "schedule"
notify:
- github_check:
name: "buildkite/nightly"
timeout_in_minutes: 60
env:
CARGO_TOOLS: nextest
secrets:
- ATUIN_TEST_ENGINE_TOKEN
plugins: *test_collector
command:
- buildkite-agent cache restore --name cargo_registry --name target --name rustup --name apt_debs
- source .buildkite/scripts/toolchain.sh
- buildkite-agent cache save --name rustup --name apt_debs
- 'section ":moon: cargo nextest (nightly)" cargo nextest run -p atuin-daemon --test nightly --run-ignored all || { .buildkite/scripts/proptest-regressions.sh; exit 1; }'
- 'section ":broom: Prune target/" .buildkite/scripts/prune-target.sh'
- buildkite-agent cache save --name target
# nix.yml: flake check, build (and run the result), formatting.
- label: ":nix: Nix flake check"
key: nix-check
notify:
- github_check:
name: "buildkite/nix-check"
timeout_in_minutes: 30
command:
- source .buildkite/scripts/lib.sh
- 'section ":nix: Install Nix" install_nix'
- 'section ":nix: nix flake check" nix flake check --print-build-logs'
- label: ":nix: Nix build"
key: nix-build
notify:
- github_check:
name: "buildkite/nix-build"
# main only: buildRustPackage builds atuin and every dependency in one
# derivation, so every change rebuilds from scratch (~4 min). The flake
# check and nixfmt steps still run on PRs.
if: build.branch == pipeline.default_branch
agents:
queue: linux-large
timeout_in_minutes: 45
command:
- source .buildkite/scripts/lib.sh
- 'section ":nix: Install Nix" install_nix'
- 'section ":nix: nix build" nix build --print-build-logs'
- 'section ":nix: Run the built binary" ./result/bin/atuin --version'
- label: ":nix: nixfmt"
key: nixfmt
notify:
- github_check:
name: "buildkite/nixfmt"
timeout_in_minutes: 15
command:
- source .buildkite/scripts/lib.sh
- 'section ":nix: Install Nix" install_nix'
- 'section ":nix: nix fmt --ci" nix fmt -- --ci'
- label: ":pencil2: Codespell"
key: codespell
notify:
- github_check:
name: "buildkite/codespell"
timeout_in_minutes: 10
command:
- source .buildkite/scripts/lib.sh
- 'section ":pencil2: Install codespell" install_codespell'
- 'section ":pencil2: codespell" codespell --count --exclude-file CHANGELOG.md'
- label: ":shell: Shellcheck"
key: shellcheck
notify:
- github_check:
name: "buildkite/shellcheck"
timeout_in_minutes: 10
env:
SHELLCHECK_OPTS: "-e SC2148"
command:
- source .buildkite/scripts/lib.sh
- 'section ":shell: Install ShellCheck" install_shellcheck'
- 'section ":shell: shellcheck" .buildkite/scripts/shellcheck.sh'
# vale.yml ran only on pull requests touching the docs or the Vale config.
# On main builds there's no diff against main, so if_changed skips it there
# too.
- label: ":memo: Vale (docs prose)"
key: vale
if_changed:
- "docs/docs/**"
- ".vale.ini"
- ".vale/styles/config/**"
- ".buildkite/scripts/vale.sh"
notify:
- github_check:
name: "buildkite/vale"
timeout_in_minutes: 10
command:
- source .buildkite/scripts/lib.sh
- 'section ":memo: Install Vale" install_vale'
- 'section ":memo: vale" .buildkite/scripts/vale.sh'
# installer.yml: the published installer (setup.atuin.sh, served from
# main's install.sh), so it runs on main builds only, as before.
- label: ":inbox_tray: Installer (Linux)"
key: install
if: build.branch == pipeline.default_branch
notify:
- github_check:
name: "buildkite/install"
timeout_in_minutes: 10
command:
- source .buildkite/scripts/lib.sh
- 'section ":shell: Install zsh" apt_install zsh'
- 'section ":inbox_tray: Installer via bash" .buildkite/scripts/test-installer.sh bash'
- 'section ":inbox_tray: Installer via zsh" .buildkite/scripts/test-installer.sh zsh'
- label: ":inbox_tray: Installer (macOS)"
key: install-macos
if: build.branch == pipeline.default_branch
notify:
- github_check:
name: "buildkite/install-macos"
agents:
queue: macos-medium
timeout_in_minutes: 10
command:
- source .buildkite/scripts/lib.sh
- 'section ":inbox_tray: Installer via bash" .buildkite/scripts/test-installer.sh bash'
- 'section ":inbox_tray: Installer via zsh" .buildkite/scripts/test-installer.sh zsh'
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# Builds atuin-client and atuin-server on a real illumos (OmniOS) VM, as
# rust.yml's "Test in VM (illumos)" job did with vmactions/omnios-vm, which
# runs anyvm.py under the hood. Hosted Linux agents expose /dev/kvm, so the
# VM is KVM-accelerated. anyvm exits with the guest command's status.
set -euo pipefail
# The versions vmactions/omnios-vm pins for this release
# (conf/r151054-build.conf).
ANYVM_VERSION=0.6.5
ANYVM_SHA256=24fbcf739fc07d7fd655f493481cb77ed022b4baa81b039227080e1a7aaaa921
OMNIOS_RELEASE=r151054-build
BUILDER_VERSION=2.1.3
RUST_VERSION=1.98.0
# shellcheck source=lib.sh disable=SC1091
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
section ":package: Install QEMU" \
apt_install qemu-system-x86 qemu-utils zstd xz-utils rsync openssh-client
section ":package: Fetch anyvm ${ANYVM_VERSION}" \
fetch_verified "https://github.com/anyvm-org/anyvm/releases/download/v${ANYVM_VERSION}/anyvm.py" \
"$ANYVM_SHA256" /tmp/anyvm.py
# Run inside the VM: the same steps as the old job's prepare + run.
guest_script="
set -e
cd /work
echo '--- :sunrise: Prepare OmniOS'
# pkg exits 4 when there is nothing to install.
pkg install pkg-config openssl || [ \$? -eq 4 ]
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs |
sh -s -- -y --profile minimal --default-toolchain ${RUST_VERSION}
. \"\$HOME/.cargo/env\"
echo '--- :hammer: cargo build -p atuin-client --release'
cargo build -p atuin-client --locked --release
echo '--- :hammer: cargo build -p atuin-server --release'
cargo build -p atuin-server --locked --release
"
# The downloaded image lives in the `anyvm` cache; --snapshot runs from it
# without copying and discards the VM's disk writes.
echo "--- :sunrise: Boot OmniOS ${OMNIOS_RELEASE} and build"
python3 /tmp/anyvm.py \
--os omnios --release "$OMNIOS_RELEASE" --builder "$BUILDER_VERSION" \
--cpu "$(nproc)" --mem 16384 \
--cache-dir "$HOME/.cache/anyvm" --data-dir /tmp/anyvm-data --snapshot \
--vnc off \
--sync rsync -v "$PWD:/work" \
-- sh -c "$guest_script"
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
# Installs ble.sh where the e2e tests expect it (~/.local/share/blesh).
set -euo pipefail
version=0.4.0-devel3
mkdir -p "$HOME/.local/share"
curl -fsSL "https://github.com/akinomyoga/ble.sh/releases/download/v${version}/ble-${version}.tar.xz" |
tar xJ -C "$HOME/.local/share"
rm -rf "$HOME/.local/share/blesh"
mv "$HOME/.local/share/ble-${version}" "$HOME/.local/share/blesh"
+120
View File
@@ -0,0 +1,120 @@
# Shared helpers, sourced (not executed) by toolchain.sh and by the steps
# that don't need Rust. Runs on Linux and macOS hosted agents, so it sticks
# to bash 3.2 (macOS's /bin/bash).
# Runs a command under its own collapsible log group. Buildkite groups all
# output after a `--- title` line until the next one; on failure `^^^ +++`
# expands the group so the error is visible.
section() {
local title=$1
shift
echo "--- $title"
"$@" && return 0
local status=$?
echo "^^^ +++"
return "$status"
}
verify_sha256() { # <sha256> <file>
if command -v sha256sum >/dev/null; then
echo "$1 $2" | sha256sum -c -
else
echo "$1 $2" | shasum -a 256 -c -
fi
}
fetch_verified() { # <url> <sha256> <file>
curl -fsSL -o "$3" "$1"
verify_sha256 "$2" "$3"
}
# Downloaded .debs, kept by the `apt_debs` cache so later jobs can install
# them with dpkg instead of paying ~5s for `apt-get update` + download.
APT_DEBS_DIR="$HOME/.cache/apt-debs"
mkdir -p "$APT_DEBS_DIR/partial"
# Installs whichever of the given apt packages are missing (Linux only).
apt_install() {
local sudo="" missing="" pkg
[ "$(id -u)" -eq 0 ] || sudo=sudo
for pkg in "$@"; do
dpkg -s "$pkg" >/dev/null 2>&1 || missing="$missing $pkg"
done
[ -n "$missing" ] || return 0
# Fast path: the .debs a previous job downloaded. --refuse-downgrade makes
# a newer base image fall through to apt rather than downgrade its
# packages to the cached versions.
if ls "$APT_DEBS_DIR"/*.deb >/dev/null 2>&1 &&
$sudo dpkg -i --refuse-downgrade "$APT_DEBS_DIR"/*.deb >/dev/null; then
echo "Installed$missing from cached .debs"
return 0
fi
# Download into APT_DEBS_DIR (not /var/cache/apt/archives, which images
# often clean after every install) so they can be cached. -f repairs
# anything a failed dpkg fast path left half-configured.
$sudo apt-get update -qq
# shellcheck disable=SC2086 # word-split the package list
$sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y -qq -f --no-install-recommends \
-o Dir::Cache::archives="$APT_DEBS_DIR" -o APT::Keep-Downloaded-Packages=true $missing
}
# nextest's `--partition count:K/N` for a step split with Buildkite's
# `parallelism: N`, and nothing for a single job, so how many ways a test
# step is split is just that one number in the pipeline.
nextest_partition() {
if [ -n "${BUILDKITE_PARALLEL_JOB_COUNT:-}" ]; then
echo "--partition count:$((BUILDKITE_PARALLEL_JOB + 1))/${BUILDKITE_PARALLEL_JOB_COUNT}"
fi
}
# Prebuilt tools not tied to Rust go here.
TOOLS_BIN="$HOME/.local/bin"
mkdir -p "$TOOLS_BIN"
export PATH="$TOOLS_BIN:$PATH"
SHELLCHECK_VERSION=0.11.0
SHELLCHECK_SHA256=b7af85e41cc99489dcc21d66c6d5f3685138f06d34651e6d34b42ec6d54fe6f6
VALE_VERSION=3.15.2
VALE_SHA256=fc72e64454d6bd7af91905d4faebbf411bae3eec17bb572f4101311212bc0d9e
CODESPELL_VERSION=2.4.3
NIX_INSTALLER_VERSION=3.22.5
NIX_INSTALLER_SHA256=9432d40ec1d0d4ebb6284848cc389bdb1bbf7aeb5c1cea11e7efc9daf0a1ec82
install_shellcheck() {
fetch_verified "https://github.com/koalaman/shellcheck/releases/download/v${SHELLCHECK_VERSION}/shellcheck-v${SHELLCHECK_VERSION}.linux.x86_64.tar.gz" \
"$SHELLCHECK_SHA256" /tmp/shellcheck.tgz
tar -xzf /tmp/shellcheck.tgz -C /tmp
mv "/tmp/shellcheck-v${SHELLCHECK_VERSION}/shellcheck" "$TOOLS_BIN/"
shellcheck --version | sed -n 2p
}
install_vale() {
fetch_verified "https://github.com/errata-ai/vale/releases/download/v${VALE_VERSION}/vale_${VALE_VERSION}_Linux_64-bit.tar.gz" \
"$VALE_SHA256" /tmp/vale.tgz
tar -xzf /tmp/vale.tgz -C "$TOOLS_BIN" vale
vale --version
}
# In its own venv: Ubuntu's system Python refuses `pip install` (PEP 668).
install_codespell() {
if ! python3 -m venv /tmp/codespell 2>/dev/null; then
apt_install python3-venv
python3 -m venv /tmp/codespell
fi
/tmp/codespell/bin/pip install --quiet "codespell==${CODESPELL_VERSION}"
ln -sf /tmp/codespell/bin/codespell "$TOOLS_BIN/codespell"
codespell --version
}
# Upstream Nix (what cachix/install-nix-action installs), root-only with no
# daemon (`--init none`), so it doesn't depend on the image running systemd.
install_nix() {
local sudo=""
[ "$(id -u)" -eq 0 ] || sudo=sudo
fetch_verified "https://github.com/DeterminateSystems/nix-installer/releases/download/v${NIX_INSTALLER_VERSION}/nix-installer-x86_64-linux" \
"$NIX_INSTALLER_SHA256" /tmp/nix-installer
chmod +x /tmp/nix-installer
$sudo /tmp/nix-installer install linux --no-confirm --init none --prefer-upstream-nix
export PATH="/nix/var/nix/profiles/default/bin:$PATH"
nix --version
}
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# Run after a failed test step: a proptest failure appends a `cc <seed>` line
# to a regression file, so show which ones changed and upload them as build
# artifacts to reproduce locally.
set -uo pipefail
echo "+++ :warning: proptest regression files changed by this run"
git add -N -- '*proptest-regressions*' 2>/dev/null || true
if git status --porcelain -- '*proptest-regressions*' | grep -q .; then
git status --porcelain -- '*proptest-regressions*'
git --no-pager diff -- '*proptest-regressions*'
buildkite-agent artifact upload '**/proptest-regressions/**;**/*.proptest-regressions'
else
echo "None."
fi
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env bash
# Trims target/ down to third-party dependency artifacts before it's saved to
# the Buildkite cache (the same idea as Swatinem/rust-cache's cleanup):
#
# * workspace crates always rebuild anyway (a fresh checkout gives every
# source file a new mtime), so their artifacts are dead weight;
# * incremental/ is unused (CARGO_INCREMENTAL=0) and doc/, package/ and
# nextest/ are per-run output;
# * final binaries at the top of each profile dir are rebuilt every run.
#
# Keeps each cache entry to roughly the size of the dependency build.
set -euo pipefail
target=${CARGO_TARGET_DIR:-target}
[ -d "$target" ] || exit 0
before=$(du -sh "$target" | cut -f1)
# Matching is done in Python (already needed to read cargo metadata) so it's
# exact and identical on Linux and macOS: an artifact belongs to the
# workspace only if its name is `<workspace name>-<16 hex hash>`, so e.g. the
# crates.io crate atuin-vt100 survives even though `atuin` is a workspace
# crate.
cargo metadata --no-deps --format-version 1 --offline | python3 -c '
import json, os, re, shutil, sys
target = sys.argv[1]
meta = json.load(sys.stdin)
names = set()
for pkg in meta["packages"]:
names |= {pkg["name"], pkg["name"].replace("-", "_")}
names |= {t["name"].replace("-", "_") for t in pkg["targets"]}
artifact = re.compile(r"^(?:lib)?(?P<name>.+?)-[0-9a-f]{16}(?:\..*)?$")
def remove(path):
if os.path.isdir(path) and not os.path.islink(path):
shutil.rmtree(path)
else:
os.remove(path)
for per_run in ("doc", "package", "nextest", "tmp"):
path = os.path.join(target, per_run)
if os.path.exists(path):
remove(path)
for profile in os.listdir(target):
profile_dir = os.path.join(target, profile)
if not os.path.isdir(os.path.join(profile_dir, "deps")):
continue
for per_run in ("incremental", "examples"):
path = os.path.join(profile_dir, per_run)
if os.path.exists(path):
remove(path)
# Top-level outputs (binaries, .d files) are copies of deps/ artifacts.
for entry in os.listdir(profile_dir):
path = os.path.join(profile_dir, entry)
if os.path.isfile(path) or os.path.islink(path):
os.remove(path)
for sub in ("deps", ".fingerprint", "build"):
sub_dir = os.path.join(profile_dir, sub)
if not os.path.isdir(sub_dir):
continue
for entry in os.listdir(sub_dir):
match = artifact.match(entry)
if match and match.group("name") in names:
remove(os.path.join(sub_dir, entry))
' "$target"
echo "Pruned $target: $before -> $(du -sh "$target" | cut -f1)"
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
# The file discovery and invocation of ludeeus/[email protected], as
# .github/workflows/shellcheck.yml ran it: every shell file by name, plus
# extensionless executables with an sh/bash/ksh shebang, each checked
# separately with SHELLCHECK_OPTS from the environment.
set -uo pipefail
excludes=(! -path '*./.git/*' ! -path '*.go' ! -path '*/mvnw')
files=()
while IFS= read -r -d '' file; do
files+=("$file")
done < <(find . "${excludes[@]}" -type f '(' \
-name '*.bash' -o -name '.bashrc' -o -name 'bashrc' -o -name '.bash_aliases' \
-o -name '.bash_completion' -o -name '.bash_login' -o -name '.bash_logout' \
-o -name '.bash_profile' -o -name 'bash_profile' -o -name '*.ksh' \
-o -name 'suid_profile' -o -name '*.zsh' -o -name '.zlogin' -o -name 'zlogin' \
-o -name '.zlogout' -o -name 'zlogout' -o -name '.zprofile' -o -name 'zprofile' \
-o -name '.zsenv' -o -name 'zsenv' -o -name '.zshrc' -o -name 'zshrc' \
-o -name '*.sh' -o -path '*/.profile' -o -path '*/profile' -o -name '*.shlib' \
')' -print0)
while IFS= read -r -d '' file; do
head -n1 "$file" | grep -Eqs '^#! */[^ ]*/(env *)?[abk]*sh' || continue
files+=("$file")
done < <(find . "${excludes[@]}" -type f ! -name '*.*' -perm /111 -print0)
echo "Checking ${#files[@]} files"
status=0
for file in "${files[@]}"; do
shellcheck --format=gcc "$file" || status=$?
done
exit "$status"
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# Starts Postgres for the integration tests on localhost:5432 (the
# ATUIN_DB_URI the step sets) and waits until it accepts connections.
set -euo pipefail
echo "--- :postgres: Starting Postgres"
docker run -d --name postgres \
-e POSTGRES_USER=atuin -e POSTGRES_PASSWORD=pass -e POSTGRES_DB=atuin \
-p 5432:5432 \
postgres:18-alpine >/dev/null
for _ in $(seq 1 30); do
if docker exec postgres pg_isready -U atuin >/dev/null 2>&1; then
echo "Postgres is ready"
exit 0
fi
sleep 1
done
docker logs postgres
echo "+++ :rotating_light: Postgres did not become ready"
exit 1
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# Runs the published installer (setup.atuin.sh, served from this repo's
# install.sh on main) under the given shell and checks atuin runs, as
# .github/workflows/installer.yml did.
set -euo pipefail
shell=$1
# Single-quoted on purpose: the inner shell expands it, not this one.
# shellcheck disable=SC2016
"$shell" -c '
/bin/bash -c "$(curl --proto "=https" --tlsv1.2 -sSf https://setup.atuin.sh)"
[ -d "$HOME/.atuin" ] && . "$HOME/.atuin/bin/env"
atuin --help
'
+90
View File
@@ -0,0 +1,90 @@
# Sourced (not executed) at the start of each Rust step: installs the Rust
# toolchain and CI tools. Shared helpers (`section` etc.) come from lib.sh.
#
# Per-step knobs, set as step env:
# RUST_TOOLCHAIN toolchain to install (default: rust-toolchain.toml)
# RUST_COMPONENTS extra rustup components, e.g. "clippy"
# CARGO_TOOLS prebuilt tools to install: "nextest", "deny"
# APT_PACKAGES extra apt packages on Linux (libssl-dev + pkg-config always)
# BREW_PACKAGES Homebrew packages on macOS
#
# Runs on Linux and macOS hosted agents, so it sticks to bash 3.2 (macOS's
# /bin/bash).
# lib.sh is checked on its own; shellcheck only follows sources with -x.
# shellcheck disable=SC1091
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
NEXTEST_VERSION=0.9.146
NEXTEST_LINUX_SHA256=682c21b777c333e96fd532e114d3a5a894e0729ab88d94c0a9f20f8419695428
NEXTEST_MACOS_SHA256=39785160b3c2f6ed9a765049cf4fa79f3b39aa02eb7598a5a0e2a1a0b9ffb9a8
CARGO_DENY_VERSION=0.20.2
CARGO_DENY_SHA256=9f12ed4c49936e09b48bf862b595cde2fe64fcbd9d74dfacac6131ca824c8d5f
echo "--- :rust: Toolchain"
export CARGO_HOME="$HOME/.cargo" RUSTUP_HOME="$HOME/.rustup"
export PATH="$CARGO_HOME/bin:$PATH"
export CARGO_TERM_COLOR=always CARGO_INCREMENTAL=0
case "$(uname -s)" in
Linux)
# shellcheck disable=SC2086 # word-split the package list
apt_install libssl-dev pkg-config ${APT_PACKAGES:-}
nextest_asset=x86_64-unknown-linux-gnu nextest_sha256=$NEXTEST_LINUX_SHA256
;;
Darwin)
if [ -n "${BREW_PACKAGES:-}" ]; then
# shellcheck disable=SC2086 # word-split the package list
HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 brew install $BREW_PACKAGES
fi
nextest_asset=universal-apple-darwin nextest_sha256=$NEXTEST_MACOS_SHA256
;;
*)
echo "Unsupported OS: $(uname -s)"
return 1
;;
esac
if ! command -v rustup >/dev/null; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs |
sh -s -- -y --no-modify-path --profile minimal --default-toolchain none
fi
if [ -n "${RUST_TOOLCHAIN:-}" ]; then
rustup toolchain install "$RUST_TOOLCHAIN" --profile minimal
export RUSTUP_TOOLCHAIN="$RUST_TOOLCHAIN"
else
# The channel pinned in rust-toolchain.toml. Named explicitly: a bare
# `rustup toolchain install` needs rustup 1.28+, and hosted images may
# ship an older one.
channel=$(sed -n 's/^channel *= *"\(.*\)"/\1/p' rust-toolchain.toml)
rustup toolchain install "$channel" --profile minimal
fi
for component in ${RUST_COMPONENTS:-}; do
rustup component add "$component"
done
# rustup may come preinstalled from elsewhere (a system package, the image),
# in which case nothing has created $CARGO_HOME/bin yet.
mkdir -p "$CARGO_HOME/bin"
for tool in ${CARGO_TOOLS:-}; do
case "$tool" in
nextest)
fetch_verified "https://github.com/nextest-rs/nextest/releases/download/cargo-nextest-${NEXTEST_VERSION}/cargo-nextest-${NEXTEST_VERSION}-${nextest_asset}.tar.gz" \
"$nextest_sha256" /tmp/nextest.tgz
tar -xzf /tmp/nextest.tgz -C "$CARGO_HOME/bin"
;;
deny)
fetch_verified "https://github.com/EmbarkStudios/cargo-deny/releases/download/${CARGO_DENY_VERSION}/cargo-deny-${CARGO_DENY_VERSION}-x86_64-unknown-linux-musl.tar.gz" \
"$CARGO_DENY_SHA256" /tmp/cargo-deny.tgz
tar -xzf /tmp/cargo-deny.tgz -C /tmp
mv "/tmp/cargo-deny-${CARGO_DENY_VERSION}-x86_64-unknown-linux-musl/cargo-deny" "$CARGO_HOME/bin/"
;;
*)
echo "Unknown CARGO_TOOLS entry: $tool"
return 1
;;
esac
done
rustc --version
cargo --version
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# Lints the docs prose as .github/workflows/vale.yml did: vale-action with
# `--minAlertLevel=warning` and reviewdog's fail_on_error, which fails on any
# reported alert. Vale alone only exits non-zero on errors, so fail on any
# output instead.
set -euo pipefail
vale sync
out=$(vale --output=line --minAlertLevel=warning docs/docs || true)
if [ -n "$out" ]; then
echo "$out"
echo "+++ :rotating_light: Vale reported $(printf '%s\n' "$out" | wc -l | tr -d ' ') warning(s) or error(s)"
exit 1
fi
echo "Vale: no warnings or errors."
+9
View File
@@ -16,3 +16,12 @@ retries = 3
# most one heavy case runs at a time with a few lightweight neighbours.
filter = 'binary_id(=atuin-daemon::scale) and test(/hundred_k/)'
threads-required = 4
# CI profile (Buildkite sets NEXTEST_PROFILE=ci). Inherits the default
# profile's settings and overrides; adds a JUnit report for Buildkite Test
# Engine.
[profile.ci.junit]
# Relative to target/nextest/ci/. Kept outside target/ so prune-target.sh
# (which deletes target/nextest/) runs before the test-collector plugin's
# pre-exit upload without taking the report with it.
path = "../../../test-results/junit.xml"
+1
View File
@@ -26,3 +26,4 @@ test-behavior.sh
# vocabulary under .vale/styles/config/ is source.
/.vale/styles/*
!/.vale/styles/config/
test-results/
+13 -12
View File
@@ -430,16 +430,6 @@ impl Sqlite {
Ok(())
}
#[instrument(level = "trace", skip_all, fields(id = ?id), err)]
async fn delete_row_raw(
tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
id: HistoryId,
) -> Result<()> {
db::query("delete from history where id = ?1").bind(id).execute(&mut **tx).await?;
Ok(())
}
#[instrument(level = "trace", skip_all, fields(id = ?h.id), err)]
pub async fn save(&self, h: &History) -> Result<()> {
debug!("saving history to sqlite");
@@ -1020,10 +1010,21 @@ impl Sqlite {
return Ok(());
}
// One `in (...)` statement per chunk that fits the bind-parameter limit, not one
// statement per id: per-row deletes made removing 200k entries take ~13s.
let ids_per_delete = self.sqlite.info().await.variable_number_limit().max(1);
let mut tx = self.sqlite.pool().begin().await?;
for id in ids {
Self::delete_row_raw(&mut tx, id).await?;
while ids.peek().is_some() {
let mut builder = sqlx::QueryBuilder::new("delete from history where id in (");
let mut list = builder.separated(", ");
for id in ids.by_ref().take(ids_per_delete) {
list.push_bind(id);
}
builder.push(")");
builder.build().execute(&mut *tx).await?;
}
tx.commit().await?;
+38 -2
View File
@@ -133,6 +133,42 @@ const BUILD_BATCH_SIZE: NonZeroUsize = NonZeroUsize::new(5000).unwrap();
/// wait on the lock and how much work a lost race throws away.
const APPEND_BATCH_SIZE: usize = 1000;
/// Below this many records, [`encrypt_records`] encrypts inline rather than fanning out.
const PARALLEL_ENCRYPT_MIN: usize = 64;
/// Encrypt `records`, in order, spread across the blocking pool. Every record gets its own content
/// key and key wrap, so encrypting a large batch one record at a time is CPU-bound on a single
/// core: it was most of the time a 200k-entry delete spent writing tombstones.
async fn encrypt_records(
records: Vec<Record<DecryptedData>>,
key: &paseto_v4::Key,
) -> Result<Vec<Record<paseto_v4::EncryptedData>>> {
if records.len() < PARALLEL_ENCRYPT_MIN {
return Ok(records.iter().map(|record| record.encrypt(key)).collect());
}
let tasks = std::thread::available_parallelism().map_or(1, NonZeroUsize::get);
let per_task = records.len().div_ceil(tasks);
let mut records = records.into_iter();
let mut handles = Vec::with_capacity(tasks);
loop {
let part: Vec<_> = records.by_ref().take(per_task).collect();
if part.is_empty() {
break;
}
let key = key.clone();
handles.push(tokio::task::spawn_blocking(move || {
part.iter().map(|record| record.encrypt(&key)).collect::<Vec<_>>()
}));
}
let mut encrypted = Vec::with_capacity(per_task * handles.len());
for handle in handles {
encrypted.extend(handle.await?);
}
Ok(encrypted)
}
/// How many records `incremental_build` decodes concurrently. Decoding is read-then-decrypt per
/// record; overlapping the reads keeps the record store's pool busy without unbounded fan-out.
/// Kept under the store's connection pool size so decodes don't starve other readers.
@@ -192,7 +228,7 @@ impl HistoryStore {
loop {
let idx = self.store.last(&series).await?.map_or(0, |p| p.idx + 1);
let encrypted: Vec<_> = chunk
let unencrypted: Vec<_> = chunk
.iter()
.enumerate()
.map(|(n, bytes)| {
@@ -203,9 +239,9 @@ impl HistoryStore {
.idx(idx + u64::conv(n))
.data(bytes.clone())
.build()
.encrypt(&self.encryption_key)
})
.collect();
let encrypted = encrypt_records(unencrypted, &self.encryption_key).await?;
if self.store.push_batch_unique(encrypted.iter()).await? {
ids.extend(encrypted.into_iter().map(|r| r.id));
@@ -57,29 +57,37 @@ impl super::Schema for Schema {
}
async fn remove(db: &Sqlite, ids: impl Iterator<Item = HistoryId>) -> Result<(), IndexError> {
let mut ids = ids.peekable();
if ids.peek().is_none() {
let keys: Vec<[u8; 16]> = ids.map(HistoryId::into_bytes).collect();
if keys.is_empty() {
return Ok(());
}
// A chunk of ids per statement rather than two statements per id: a bulk history delete
// passes every id, most of which never had output indexed, and 400k single-row
// statements made removing 200k ids take ~20s.
let keys_per_delete = db.info().await.variable_number_limit().max(1);
let pool = db.pool();
let mut tx = pool.begin_with("BEGIN IMMEDIATE").await.map_err(store)?;
for id in ids {
let key = id.into_bytes();
db::query(
"DELETE FROM output_fts WHERE rowid = (SELECT id FROM indexed WHERE history_id = \
?)",
)
.bind(&key[..])
.execute(&mut *tx)
.await
.map_err(store)?;
db::query("DELETE FROM indexed WHERE history_id = ?")
.bind(&key[..])
.execute(&mut *tx)
.await
.map_err(store)?;
for chunk in keys.chunks(keys_per_delete) {
let mut fts = sqlx::QueryBuilder::new(
"DELETE FROM output_fts WHERE rowid IN (SELECT id FROM indexed WHERE history_id \
IN (",
);
let mut list = fts.separated(", ");
for key in chunk {
list.push_bind(&key[..]);
}
fts.push("))");
fts.build().execute(&mut *tx).await.map_err(store)?;
let mut indexed = sqlx::QueryBuilder::new("DELETE FROM indexed WHERE history_id IN (");
let mut list = indexed.separated(", ");
for key in chunk {
list.push_bind(&key[..]);
}
indexed.push(")");
indexed.build().execute(&mut *tx).await.map_err(store)?;
}
tx.commit().await.map_err(store)?;