Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ jobs:
- name: go vet
run: go vet ./...

- name: go vet (build-tagged code)
run: |
set -euo pipefail
go vet -tags=load ./...
go vet -tags=e2e ./...

- name: go test (race + cover)
run: go test -race -coverprofile=coverage.out ./...

Expand Down
35 changes: 19 additions & 16 deletions cmd/artemis/bootrun_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/testcontainers/testcontainers-go/wait"

"github.com/freeCodeCamp/artemis/internal/config"
"github.com/freeCodeCamp/artemis/internal/config/configtest"
"github.com/freeCodeCamp/artemis/internal/pg"
vkstore "github.com/freeCodeCamp/artemis/internal/registry/valkey"
)
Expand Down Expand Up @@ -322,22 +323,24 @@ func TestRun_BootsFromEnvAndExitsOnSigterm(t *testing.T) {
dsn, valkeyAddr := startDeps(t)
port := freePort(t)

t.Setenv("PORT", strconv.Itoa(port))
t.Setenv("DATABASE_URL", dsn)
t.Setenv("VALKEY_ADDR", valkeyAddr)
t.Setenv("R2_ENDPOINT", "http://127.0.0.1:1")
t.Setenv("R2_ACCESS_KEY_ID", "k")
t.Setenv("R2_SECRET_ACCESS_KEY", "s")
t.Setenv("R2_BUCKET", "b")
t.Setenv("GH_CLIENT_ID", "cid")
t.Setenv("JWT_SIGNING_KEY", "0123456789abcdef0123456789abcdef")
t.Setenv("DEPLOY_PREFIX_FORMAT", "<site>.example.test/deploys/<ts>-<sha>/")
t.Setenv("ALIAS_PRODUCTION_KEY_FORMAT", "<site>.example.test/production")
t.Setenv("ALIAS_PREVIEW_KEY_FORMAT", "<site>.example.test/preview")
t.Setenv("LOG_LEVEL", "error")
t.Setenv("SENTRY_DSN", "https://publickey@o0.ingest.sentry.io/0")
t.Setenv("ENVIRONMENT", "test")
t.Setenv("SENTRY_TRACES_SAMPLE_RATE", "0")
configtest.Hermetic(t, config.EnvKeys(), map[string]string{
"PORT": strconv.Itoa(port),
"DATABASE_URL": dsn,
"VALKEY_ADDR": valkeyAddr,
"R2_ENDPOINT": "http://127.0.0.1:1",
"R2_ACCESS_KEY_ID": "k",
"R2_SECRET_ACCESS_KEY": "s",
"R2_BUCKET": "b",
"GH_CLIENT_ID": "cid",
"JWT_SIGNING_KEY": "0123456789abcdef0123456789abcdef",
"DEPLOY_PREFIX_FORMAT": "<site>.example.test/deploys/<ts>-<sha>/",
"ALIAS_PRODUCTION_KEY_FORMAT": "<site>.example.test/production",
"ALIAS_PREVIEW_KEY_FORMAT": "<site>.example.test/preview",
"LOG_LEVEL": "error",
"SENTRY_DSN": "https://publickey@o0.ingest.sentry.io/0",
"ENVIRONMENT": "test",
"SENTRY_TRACES_SAMPLE_RATE": "0",
})

done := make(chan error, 1)
go func() { done <- run() }()
Expand Down
2 changes: 1 addition & 1 deletion cmd/artemis/driftreport.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ func driftReportSites(ctx context.Context, repo siteDirnameReader, reg registryS
if err != nil {
return nil, err
}
slugs := make([]string, 0, len(sites))
slugs := make([]sitekey.Slug, 0, len(sites))
for _, s := range sites {
slugs = append(slugs, s.Slug)
}
Expand Down
23 changes: 13 additions & 10 deletions cmd/artemis/driftreport_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/freeCodeCamp/artemis/internal/config"
"github.com/freeCodeCamp/artemis/internal/config/configtest"
"github.com/freeCodeCamp/artemis/internal/pg"

"github.com/freeCodeCamp/artemis/internal/sitekey"
)

Expand Down Expand Up @@ -112,15 +113,17 @@ func (f *fakeBucket) listV2(w http.ResponseWriter, prefix string) {

func driftReportEnv(t *testing.T, dsn, endpoint string) {
t.Helper()
t.Setenv("DATABASE_URL", dsn)
t.Setenv("R2_ENDPOINT", endpoint)
t.Setenv("R2_BUCKET", "artemis-test")
t.Setenv("R2_ACCESS_KEY_ID", "k")
t.Setenv("R2_SECRET_ACCESS_KEY", "s")
t.Setenv("DEPLOY_PREFIX_FORMAT", "<site>/deploys/<ts>-<sha>/")
t.Setenv("GH_CLIENT_ID", "cid")
t.Setenv("JWT_SIGNING_KEY", "0123456789abcdef0123456789abcdef")
t.Setenv("VALKEY_ADDR", "127.0.0.1:1")
configtest.Hermetic(t, config.EnvKeys(), map[string]string{
"DATABASE_URL": dsn,
"R2_ENDPOINT": endpoint,
"R2_BUCKET": "artemis-test",
"R2_ACCESS_KEY_ID": "k",
"R2_SECRET_ACCESS_KEY": "s",
"DEPLOY_PREFIX_FORMAT": "<site>/deploys/<ts>-<sha>/",
"GH_CLIENT_ID": "cid",
"JWT_SIGNING_KEY": "0123456789abcdef0123456789abcdef",
"VALKEY_ADDR": "127.0.0.1:1",
})
}

func seedDriftFixture(t *testing.T, dsn string, site sitekey.Dirname, deployID string) {
Expand Down
4 changes: 2 additions & 2 deletions cmd/artemis/driftreport_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ func (r fakeDirnameReader) KnownSiteDirnames(context.Context) ([]sitekey.Dirname
return r.sites, nil
}

type fakeRegistryReader struct{ slugs []string }
type fakeRegistryReader struct{ slugs []sitekey.Slug }

func (r fakeRegistryReader) Sites(context.Context) ([]registry.Site, error) {
out := make([]registry.Site, 0, len(r.slugs))
Expand All @@ -137,7 +137,7 @@ func TestDriftReportSites_CoversSitesTheSchedulerCannotSee(t *testing.T) {

sites, err := driftReportSites(context.Background(),
fakeDirnameReader{sites: []sitekey.Dirname{"orphan.freecode.camp", "www.freecode.camp"}},
fakeRegistryReader{slugs: []string{"www", "quiet"}},
fakeRegistryReader{slugs: []sitekey.Slug{"www", "quiet"}},
tmpl)
require.NoError(t, err)

Expand Down
4 changes: 2 additions & 2 deletions cmd/artemis/gcworkflows.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,13 +178,13 @@ func registerGCWorkflows(rt workflowRegistrar, gcw *gcWiring, dryRun bool, sweep
return nil
}

func storageSiteNames(slugs []string, tmpl handler.DeployPrefixTemplate) []sitekey.Dirname {
func storageSiteNames(slugs []sitekey.Slug, tmpl handler.DeployPrefixTemplate) []sitekey.Dirname {
if len(slugs) == 0 {
return nil
}
names := make([]sitekey.Dirname, 0, len(slugs))
for _, s := range slugs {
names = append(names, tmpl.SiteDirname(sitekey.Slug(s)))
names = append(names, tmpl.SiteDirname(s))
}
return names
}
Expand Down
8 changes: 4 additions & 4 deletions cmd/artemis/reconcile_keyspace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ func TestStorageSiteNames_ProduceThePrefixTheWritePathUsed(t *testing.T) {
layout, err := newGCLayout(domainFormat, "_trash/")
require.NoError(t, err)

slugs := []string{"test", "hello-universe", "flag-frenzy"}
slugs := []sitekey.Slug{"test", "hello-universe", "flag-frenzy"}
names := storageSiteNames(slugs, tmpl)
require.Len(t, names, len(slugs))

for i, slug := range slugs {
require.Equal(t, tmpl.SitePrefix(sitekey.Slug(slug)), layout.sitePrefix(names[i]),
require.Equal(t, tmpl.SitePrefix(slug), layout.sitePrefix(names[i]),
"slug %q: reconcile would list a prefix no deploy is stored under", slug)
}
}
Expand All @@ -38,7 +38,7 @@ func TestStorageSiteNames_MatchTheOutboxSiteChangedForm(t *testing.T) {

require.Equal(t,
[]sitekey.Dirname{tmpl.SiteDirname("test")},
storageSiteNames([]string{"test"}, tmpl))
storageSiteNames([]sitekey.Slug{"test"}, tmpl))
}

func TestStorageSiteNames_EmptyRegistryYieldsNoNames(t *testing.T) {
Expand All @@ -57,7 +57,7 @@ func TestStorageSiteNames_BareFormatIsIdentity(t *testing.T) {
layout, err := newGCLayout("<site>/deploys/<ts>-<sha>/", "_trash/")
require.NoError(t, err)

names := storageSiteNames([]string{"test", "www"}, tmpl)
names := storageSiteNames([]sitekey.Slug{"test", "www"}, tmpl)
require.Equal(t, []sitekey.Dirname{"test", "www"}, names)
require.Equal(t, tmpl.SitePrefix("test"), layout.sitePrefix(names[0]))
}
Expand Down
17 changes: 11 additions & 6 deletions cmd/loadgen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

"github.com/freeCodeCamp/artemis/internal/gc"
"github.com/freeCodeCamp/artemis/internal/pg"
"github.com/freeCodeCamp/artemis/internal/sitekey"
"github.com/freeCodeCamp/artemis/internal/worker"
)

Expand Down Expand Up @@ -119,7 +120,7 @@ func runDeploys(ctx context.Context, cfg config, repo *pg.Repo) stageResult {
total := cfg.sites * cfg.deploysPerSite
base := time.Now().Add(-90 * 24 * time.Hour)
return drive("deploy_upsert", total, cfg.concurrency, func(i int) error {
site := siteSlug(i % cfg.sites)
site := siteDirname(i % cfg.sites)
seq := i / cfg.sites
id := fmt.Sprintf("%d-%08x", base.Add(time.Duration(seq)*time.Hour).Unix(), i)
return repo.UpsertDeploy(ctx, site, id, base.Add(time.Duration(seq)*time.Hour), 1<<20, true, "active")
Expand All @@ -128,7 +129,7 @@ func runDeploys(ctx context.Context, cfg config, repo *pg.Repo) stageResult {

func runOutboxEnqueue(ctx context.Context, cfg config, repo *pg.Repo) stageResult {
return drive("outbox_enqueue", cfg.sites, cfg.concurrency, func(i int) error {
return repo.EnqueueSiteChanged(ctx, siteSlug(i))
return repo.EnqueueSiteChanged(ctx, siteDirname(i))
})
}

Expand Down Expand Up @@ -160,12 +161,12 @@ func runGCPlan(ctx context.Context, cfg config, repo *pg.Repo) stageResult {
Mover: nopMover{},
Policy: gc.Policy{RecentKeep: 10, Grace: 24 * time.Hour, Retention: 30 * 24 * time.Hour, ServeCacheTTL: time.Hour},
BlastCap: 1000,
DeployPrefix: func(site, id string) string { return site + "/deploys/" + id + "/" },
TrashPrefix: func(site, id string) string { return "_trash/" + site + "/" + id + "/" },
DeployPrefix: func(site sitekey.Dirname, id string) string { return string(site) + "/deploys/" + id + "/" },
TrashPrefix: func(site sitekey.Dirname, id string) string { return "_trash/" + string(site) + "/" + id + "/" },
Now: time.Now,
}
return drive("gc_plan_dryrun", cfg.sites, cfg.concurrency, func(i int) error {
_, err := g.Run(ctx, siteSlug(i), true)
_, err := g.Run(ctx, siteDirname(i), true)
return err
})
}
Expand Down Expand Up @@ -236,7 +237,11 @@ func truncate(ctx context.Context, db *pg.DB) error {
return err
}

func siteSlug(i int) string { return fmt.Sprintf("loadgen-site-%06d.freecode.camp", i) }
func siteSlug(i int) sitekey.Slug { return sitekey.Slug(fmt.Sprintf("loadgen-site-%06d", i)) }

func siteDirname(i int) sitekey.Dirname {
return sitekey.Dirname(string(siteSlug(i)) + ".freecode.camp")
}

type nopPublisher struct{}

Expand Down
2 changes: 1 addition & 1 deletion docs/design/0003-postgres-durability.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Loss is NOT a serving outage: the serve plane (Caddy `r2_alias` → R2) never to
| Disk loss on the node | no — same as node loss |
| R2 bucket loss (backup target) | out of scope here — R2 is the platform's own durability domain |

RPO today: up to 24 h. RTO today: manual — new PVC + `psql < dump` + repoint; unrehearsed (unverified — no restore drill is recorded anywhere in this repo or the infra runbooks).
RPO today: up to 24 h. RTO today: manual — new PVC + `psql < dump` + repoint — against a stated floor of \<= 60 min. The restore leg **is** rehearsed: `infra:docs/runbooks/08-artemis-pg-restore-drill.md` records the R8 drill PASSED on 2026-06-05, restoring the newest R2 dump into a scratch Postgres with both tenants back and 6/6 artemis tables present. What is not rehearsed is the StatefulSet re-provision that precedes it; that is the remaining wall-time inside the 60 min (runbook 08 §F).

## 3. Options scored

Expand Down
Loading
Loading