@marcel91200 / caddy Mirror · read-only / commits / 425a3381
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 04:45 UTC

acmeserver: say when the CA database is locked by another process (#8007)

Opening the bbolt database only reported timeout, which sounds like a broken config. Catch bolt.ErrTimeout with errors.Is and say another process holds the lock, and that caddy reload is the right command when Caddy is already running. The error stays fatal.

Faiyaz Rahman committed Sep 11, 2026 at 04:49 UTC 425a3381fdf65c826dd991c92e78eaf6ef24941e
3 files changed +52 -1
go.mod
+1 -1
@@ -165,7 +165,7 @@ require (
165 github.com/slackhq/nebula v1.10.3 // indirect
166 github.com/spf13/cast v1.7.0 // indirect
167 github.com/urfave/cli v1.22.17 // indirect
168 - go.etcd.io/bbolt v1.4.3 // indirect
168 + go.etcd.io/bbolt v1.4.3
169 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect
170 go.opentelemetry.io/otel/metric v1.44.0 // indirect
171 go.opentelemetry.io/otel/trace v1.44.0
modules/caddypki/acmeserver/acmeserver.go
+6
@@ -16,6 +16,7 @@ package acmeserver
16
17 import (
18 "context"
19 + "errors"
20 "fmt"
21 weakrand "math/rand/v2"
22 "net"
@@ -34,6 +35,7 @@ import (
35 "github.com/smallstep/certificates/authority/provisioner"
36 "github.com/smallstep/certificates/db"
37 "github.com/smallstep/nosql"
38 + bolterrors "go.etcd.io/bbolt/errors"
39 "go.uber.org/zap"
40 "go.uber.org/zap/zapcore"
41
@@ -290,6 +292,10 @@ func (ash Handler) openDatabase() (*db.AuthDB, error) {
292 DataSource: dbPath,
293 }
294 database, err := db.New(dbConfig)
295 + if errors.Is(err, bolterrors.ErrTimeout) {
296 + // bbolt holds an exclusive file lock, so something else has this database open
297 + err = fmt.Errorf("%w: CA database %s is already locked; this is usually another running Caddy instance, in which case use 'caddy reload' to apply config changes to it", err, dbPath)
298 + }
299 return databaseCloser{&database}, err
300 })
301
modules/caddypki/acmeserver/acmeserver_test.go
+45
@@ -1,9 +1,15 @@
1 package acmeserver
2
3 import (
4 + "errors"
5 + "os"
6 + "path/filepath"
7 "strings"
8 "testing"
9 + "time"
10
11 + bolt "go.etcd.io/bbolt"
12 + bolterrors "go.etcd.io/bbolt/errors"
13 "go.uber.org/zap"
14 "go.uber.org/zap/zaptest/observer"
15 )
@@ -92,3 +98,42 @@ func TestHandler_warnIfPolicyAllowsAll(t *testing.T) {
98 })
99 }
100 }
101 +
102 +func TestHandler_openDatabaseLocked(t *testing.T) {
103 + if testing.Short() {
104 + t.Skip("skipping: bbolt waits 5s for the lock")
105 + }
106 +
107 + dataDir := t.TempDir()
108 + t.Setenv("XDG_DATA_HOME", dataDir)
109 +
110 + ash := Handler{CA: "locked-db-test", logger: zap.NewNop()}
111 + key := ash.getDatabaseKey()
112 +
113 + dbFolder := filepath.Join(dataDir, "caddy", "acme_server", key)
114 + if err := os.MkdirAll(dbFolder, 0o755); err != nil {
115 + t.Fatalf("making database folder: %v", err)
116 + }
117 + dbPath := filepath.Join(dbFolder, "db")
118 +
119 + // hold the lock like a running Caddy instance would
120 + locked, err := bolt.Open(dbPath, 0o600, &bolt.Options{Timeout: 5 * time.Second})
121 + if err != nil {
122 + t.Fatalf("opening database: %v", err)
123 + }
124 + defer locked.Close()
125 +
126 + _, err = ash.openDatabase()
127 + if err == nil {
128 + t.Fatal("expected an error opening a locked database, got none")
129 + }
130 + if !errors.Is(err, bolterrors.ErrTimeout) {
131 + t.Errorf("expected error to wrap bolterrors.ErrTimeout, got: %v", err)
132 + }
133 + if !strings.Contains(err.Error(), "already locked") {
134 + t.Errorf("expected error to explain the lock, got: %v", err)
135 + }
136 + if _, err := databasePool.Delete(key); err != nil {
137 + t.Errorf("cleaning up database pool: %v", err)
138 + }
139 +}