Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 118 additions & 2 deletions .github/workflows/rust-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -248,17 +248,20 @@ jobs:

- name: Run unit tests
env:
CARGO_TERM_COLOR: never
RUST_BACKTRACE: 1
CARGO_INCREMENTAL: 0
run: |
set -o pipefail

# Run tests, capturing the names of any failures for targeted retry
if cargo test --no-fail-fast --lib ${{ matrix.crates }} 2>&1 | tee /tmp/test-output.txt; then
echo "All tests passed on first attempt"
exit 0
fi

# Extract failed test names from output
failed_tests=$(grep '^test .* FAILED$' /tmp/test-output.txt | sed 's/^test \(.*\) \.\.\..*FAILED$/\1/' | tr '\n' ' ')
failed_tests=$(grep '^test .* FAILED$' /tmp/test-output.txt | sed 's/^test \(.*\) \.\.\..*FAILED$/\1/' | tr '\n' ' ' || true)
if [ -z "$failed_tests" ]; then
echo "Could not parse failed test names, re-running all tests"
cargo test --no-fail-fast --lib ${{ matrix.crates }}
Expand Down Expand Up @@ -444,10 +447,13 @@ jobs:

- name: Run integration tests
env:
CARGO_TERM_COLOR: never
RUST_BACKTRACE: 1
CARGO_INCREMENTAL: 0
TEMPS_TEST_DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db
run: |
set -o pipefail

# Run tests, capturing the names of any failures for targeted retry.
# `cargo_args` covers everything cargo cares about (crate, features,
# filters); `test_args` is everything after `--` that libtest cares
Expand All @@ -460,7 +466,7 @@ jobs:
fi

# Extract failed test names from output
failed_tests=$(grep '^test .* FAILED$' /tmp/test-output.txt | sed 's/^test \(.*\) \.\.\..*FAILED$/\1/' | tr '\n' ' ')
failed_tests=$(grep '^test .* FAILED$' /tmp/test-output.txt | sed 's/^test \(.*\) \.\.\..*FAILED$/\1/' | tr '\n' ' ' || true)
if [ -z "$failed_tests" ]; then
echo "Could not parse failed test names, re-running all tests"
cargo ${{ matrix.cargo_args }} -- ${{ matrix.test_args }}
Expand Down Expand Up @@ -494,3 +500,113 @@ jobs:
sleep 10
done
done

# ---------------------------------------------------------------------------
# Phase 2b: MariaDB PITR full-chain E2E
# ---------------------------------------------------------------------------
# Keep this heavyweight single test out of the generic Docker matrix so it can
# start immediately, and so Actions shows each expensive phase (image pull,
# test build, test run, diagnostics) separately.
mariadb-pitr-e2e:
name: "MariaDB PITR E2E"
runs-on: ubuntu-latest
timeout-minutes: 35

services:
timescaledb:
image: timescale/timescaledb-ha:pg18
env:
POSTGRES_DB: test_db
POSTGRES_USER: test_user
POSTGRES_PASSWORD: test_password
POSTGRES_HOST_AUTH_METHOD: trust
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U test_user -d test_db"
--health-interval 10s
--health-timeout 5s
--health-retries 5

steps:
- name: Free up disk space
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
sudo docker image prune --all --force
sudo apt-get autoremove -y && sudo apt-get clean
df -h

- name: Checkout code
uses: actions/checkout@v4

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Restore build cache
uses: Swatinem/rust-cache@v2
with:
shared-key: test-build
save-if: false

- name: Install system dependencies
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler

- name: Verify TimescaleDB is ready
run: |
timeout 60 bash -c 'until nc -z localhost 5432; do sleep 1; done'
echo "TimescaleDB is ready"

- name: Show Docker environment
run: |
docker version
docker info
docker system df

- name: Pull E2E images
run: |
timeout 10m docker pull minio/minio:latest
timeout 10m docker pull mariadb:lts
docker image ls | grep -E '^(minio/minio|mariadb)\s' || true

- name: Build MariaDB PITR E2E binary
env:
CARGO_TERM_COLOR: never
CARGO_INCREMENTAL: 0
run: |
cargo test -p temps-backup --features docker-tests mariadb_pitr_full_chain_e2e --no-run

- name: Run MariaDB PITR E2E
env:
CARGO_TERM_COLOR: never
RUST_BACKTRACE: 1
CARGO_INCREMENTAL: 0
TEMPS_TEST_DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db
run: |
timeout --foreground 20m bash -lc 'set -o pipefail; cargo test -p temps-backup --features docker-tests mariadb_pitr_full_chain_e2e -- --nocapture --test-threads=1 2>&1 | tee /tmp/mariadb-pitr-e2e.log'

- name: Summarize MariaDB PITR E2E diagnostics
if: always()
run: |
if [ -f /tmp/mariadb-pitr-e2e.log ]; then
grep -E 'Restored row counts|test result|FAILED|Timed out|timed out|Fetched MariaDB PITR|Uploading MariaDB PITR|Uploaded MariaDB PITR|Replaying MariaDB|temps-mariadb-pitr-replay|docker exec timed out|DIAG|Booted MariaDB|Base backup|archive_binlogs|Restore produced' /tmp/mariadb-pitr-e2e.log | tail -n 240 || true
else
echo "No MariaDB PITR E2E log was written"
fi

echo ""
echo "Docker containers:"
docker ps -a || true
echo ""
echo "Docker images:"
docker image ls || true
echo ""
echo "Docker disk usage:"
docker system df || true

- name: Upload MariaDB PITR E2E log
if: always()
uses: actions/upload-artifact@v4
with:
name: mariadb-pitr-e2e-log
path: /tmp/mariadb-pitr-e2e.log
if-no-files-found: ignore
84 changes: 81 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,92 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
-
- **Opt-in MariaDB external services**: Temps can now create standalone
MariaDB services for hosted projects without changing its internal Postgres
dependency. The new `mariadb` service type uses the official `mariadb:lts`
container image, generates separate application and root passwords, provisions
per-project/per-environment databases, and exposes both `MYSQL_*` and
`MARIADB_*` runtime environment variables plus `DATABASE_URL`. MariaDB is
supported in the query explorer, existing MariaDB/MySQL-compatible containers
can be imported as MariaDB services, and full logical backup/restore uses
`mariadb-dump` with `mysqldump` fallback for non-system databases.

### Changed
-
- **MariaDB services default to a small-host profile**: new managed MariaDB
services now materialize `size_profile=small` into a conservative `resources`
block (`512` MiB memory, `768` MiB memory+swap, `750000000` nano-cpus) and
start `mariadbd` with tuned connection/cache/buffer settings. The create
service UI now presents MariaDB as a shared database server whose linked
projects get separate databases, reducing accidental one-container-per-website
installs on 4 GiB and 8 GiB hosts.

### Fixed
-
- **Laravel MariaDB docs use runtime service env vars**: the Laravel tutorial
now keeps `php artisan config:cache` out of the Docker build so Temps-injected
runtime variables are not baked incorrectly into the image, documents
`DATABASE_URL`/`MYSQL_*`/`MARIADB_*` fallbacks for MariaDB-backed Laravel
configs, and keeps `php artisan migrate --force` as a one-off release step
rather than a per-container start command.
- **MariaDB physical base backups are gzipped correctly**: `mariadb_physical`
now binds the `| gzip` pipeline directly to `mariadb-backup --stream=mbstream`
instead of the trailing scratch-directory cleanup command, so base backup
objects ending in `base.mbstream.gz` are valid gzip streams that PITR restore
can unpack.
- **MariaDB PITR restore reads source binlogs for new services**:
restore-to-new-service now fetches the binlog manifest and archived binlog
objects from the source service name prefix instead of the newly restored
service name, allowing forward replay to find the source service's archived
segments.
- **MariaDB physical restore helper waits are bounded**: PITR physical restore
now times out a stuck helper container wait with diagnostic logs instead of
hanging indefinitely, so failed restore attempts surface actionable errors.
- **MariaDB physical restore diagnostics cannot hang**: restore-helper log
collection is now finite and includes phase markers, so a stuck PITR physical
restore reports the helper phase instead of masking the timeout.
- **MariaDB restore-to-new-service avoids redundant image pulls**: MariaDB
container creation now reuses a locally present `docker_image` and bounds the
pull when the image is missing, preventing restore provisioning from hanging
on an unnecessary network pull.
- **MariaDB PITR binlog replay is bounded and non-interactive**: PITR replay now
uses an explicit TCP root connection, a short dedicated replay timeout,
per-phase `timeout` guards, and replay phase markers so a stuck or
prompt-bound `mariadb-binlog`/client invocation fails with context instead of
hanging behind the generic backup timeout.
- **MariaDB PITR binlog uploads are bounded**: archived binlog segments copied
into the restored container now use a dedicated upload timeout and phase logs,
so Docker archive-upload stalls fail before the E2E or job timeout.
- **Docker exec API calls are bounded for provider operations**: shared
`externalsvc::exec_util::run_exec` now applies a short Docker API timeout to
exec creation, start, polling, and final log draining while preserving
captured command output on command timeouts. It also stops polling a drained
output stream, preventing restore workflows from hanging when Docker closes
stdout/stderr before `inspect_exec` reports completion.

### Tests
- **Full-chain MariaDB PITR Docker E2E coverage**: `crates/temps-backup/tests/mariadb_pitr_e2e.rs`
exercises the real physical backup engine, S3 binlog archiver, and
`restore_pitr(Time)` path against Dockerized MariaDB, MinIO, and Postgres,
asserting that rows before the recovery timestamp are restored while later
rows are excluded. The `docker-tests` feature and `docker-backup` GitHub
Actions matrix group keep the heavyweight test opt-in locally and enforced in
CI.
- **GitHub Actions cargo test pipelines preserve failures**: the Rust test
workflow now enables `pipefail` before piping cargo test output through
`tee`, disables cargo color for parseable test result lines, and makes failed
test extraction non-fatal, so failed unit or integration tests cannot be
reported as successful before retry parsing runs.
- **MariaDB PITR E2E does not retry after long failures**: the `docker-backup`
GitHub Actions group now fails immediately after a failed full-chain E2E
attempt, preserving the first diagnostic log instead of spending another
timeout window rerunning the same expensive scenario.
- **MariaDB PITR E2E emits provider phase logs**: the Docker E2E initializes a
test-only tracing subscriber for `temps_providers::externalsvc::mariadb`, so
CI logs show where a full-chain restore is spending time.
- **MariaDB PITR E2E runs as a dedicated CI job**: `.github/workflows/rust-tests.yml`
now runs the full-chain Docker E2E outside the generic integration matrix,
starts it immediately, and gives it explicit image-pull, binary-build,
test-run, and diagnostics steps, so GitHub Actions exposes progress and
hard-stops stuck runs faster.

## [0.1.0-beta.39] - 2026-06-25

Expand Down
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ sea-orm-migration = { version = "1.1", features = [
"sqlx-postgres",
"runtime-tokio-rustls"
] }
sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio"] }
sqlx = { version = "0.8.6", features = ["postgres", "mysql", "runtime-tokio"] }
rusqlite = { version = "0.32.0", features = ["bundled", "trace"] }

# ============================================================================
Expand Down
10 changes: 10 additions & 0 deletions crates/temps-backup/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ async-trait = { workspace = true }
async-stream = { workspace = true }
urlencoding = { workspace = true }

[features]
# Docker-dependent integration tests. Mirrors temps-providers: gated behind a
# feature so `cargo test` on a host without a reachable Docker socket skips
# the heavy end-to-end tests, while CI opts in explicitly. The test files
# themselves also skip gracefully at runtime when Docker is unreachable.
docker-tests = []

[dev-dependencies]
sea-orm = { workspace = true, features = ["mock"] }
testcontainers = { workspace = true }
sqlx = { workspace = true }
temps-migrations = { path = "../temps-migrations" }
tracing-subscriber = { workspace = true }
Loading