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
+}