Skip to content

Commit 78ef5f1

Browse files
committed
testserver: replace KillCaller config with HTTP kill API
Move kill-on-request behavior from test.toml fields (KillCaller, KillCallerOffset) to a POST /__testserver/kill endpoint. Kill rules are scoped by auth token so concurrent tests sharing a server don't interfere. acceptance/bin/kill_after.py is a convenience wrapper that posts to the endpoint, keeping scripts readable. The kill check is applied at the HTTP middleware layer (wrapping the entire router) so it fires for all requests, including those that would otherwise fall through to the not-found handler. Co-authored-by: Isaac
1 parent eca0376 commit 78ef5f1

24 files changed

Lines changed: 198 additions & 125 deletions

File tree

acceptance/bin/kill_after.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/usr/bin/env python3
2+
"""Set up a kill rule on the testserver for the current test token.
3+
4+
Usage: kill_after.py PATTERN OFFSET TIMES
5+
6+
PATTERN HTTP method and path, e.g. "POST /api/2.2/jobs/create"
7+
OFFSET number of requests to let through before killing starts
8+
TIMES number of times to kill the caller
9+
10+
The rule is scoped to the current DATABRICKS_TOKEN so it only affects
11+
the test that registers it, even when tests share a server.
12+
"""
13+
14+
import json
15+
import os
16+
import sys
17+
import urllib.request
18+
19+
host = os.environ.get("DATABRICKS_HOST", "")
20+
token = os.environ.get("DATABRICKS_TOKEN", "")
21+
22+
if not host:
23+
print("DATABRICKS_HOST not set", file=sys.stderr)
24+
sys.exit(1)
25+
26+
if len(sys.argv) != 4:
27+
print(f"usage: {sys.argv[0]} PATTERN OFFSET TIMES", file=sys.stderr)
28+
sys.exit(1)
29+
30+
pattern, offset, times = sys.argv[1], int(sys.argv[2]), int(sys.argv[3])
31+
32+
data = json.dumps({"pattern": pattern, "offset": offset, "times": times}).encode()
33+
req = urllib.request.Request(
34+
f"{host}/__testserver/kill",
35+
data=data,
36+
headers={"Content-Type": "application/json", "Authorization": f"Bearer {token}"},
37+
method="POST",
38+
)
39+
urllib.request.urlopen(req)

acceptance/bundle/deploy/wal/chain-3-jobs/script

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
kill_after.py "POST /api/2.2/jobs/create" 2 1
2+
13
echo "=== First deploy (crashes on job_03) ==="
24
trace errcode $CLI bundle deploy
35

acceptance/bundle/deploy/wal/chain-3-jobs/test.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33

44
[[Server]]
55
Pattern = "POST /api/2.2/jobs/create"
6-
KillCallerOffset = 2
7-
KillCaller = 1
86
Response.Body = '{"job_id": 1001}'
97

108
[[Server]]

acceptance/bundle/deploy/wal/crash-after-create/script

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
kill_after.py "GET /api/2.2/jobs/get" 0 1
2+
13
echo "=== First deploy (crashes after job_a create, before job_b) ==="
24
trace errcode $CLI bundle deploy
35

acceptance/bundle/deploy/wal/crash-after-create/test.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,4 @@ Response.Body = '{}'
1212

1313
[[Server]]
1414
Pattern = "GET /api/2.2/jobs/get"
15-
KillCaller = 1
1615
Response.Body = '{"job_id": 1001, "settings": {"name": "test-job"}}'

acceptance/internal/config.go

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -153,18 +153,6 @@ type ServerStub struct {
153153
// Configure as "1ms", "2s", "3m", etc.
154154
// See [time.ParseDuration] for details.
155155
Delay time.Duration
156-
157-
// Number of times to kill the caller process before returning normal responses.
158-
// 0 = never kill (default), 1 = kill once then allow, 2 = kill twice then allow, etc.
159-
// Useful for testing crash recovery scenarios where first deploy crashes but retry succeeds.
160-
// Requires DATABRICKS_CLI_TEST_PID=1 to be set in the test environment.
161-
KillCaller int
162-
163-
// Number of requests to let pass before starting to kill.
164-
// Combined with KillCaller, this creates a window: requests 1 to Offset succeed,
165-
// requests Offset+1 to Offset+KillCaller are killed, rest succeed.
166-
// Example: KillCallerOffset=9, KillCaller=1 means let 9 requests pass, kill the 10th.
167-
KillCallerOffset int
168156
}
169157

170158
// FindConfigs finds all the config relevant for this test,

acceptance/internal/prepare_server.go

Lines changed: 0 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -183,10 +183,6 @@ func startLocalServer(t *testing.T,
183183
s.ResponseCallback = logResponseCallback(t)
184184
}
185185

186-
killCounters := make(map[string]int)
187-
offsetCounters := make(map[string]int)
188-
killCountersMu := &sync.Mutex{}
189-
190186
for ind := range stubs {
191187
// Later stubs take precedence over earlier ones (leaf configs override parent configs).
192188
// The first handler registered for a given pattern wins, so we reverse the order.
@@ -195,11 +191,6 @@ func startLocalServer(t *testing.T,
195191
items := strings.Split(stub.Pattern, " ")
196192
require.Len(t, items, 2)
197193

198-
if stub.KillCaller > 0 {
199-
killCounters[stub.Pattern] = stub.KillCaller
200-
offsetCounters[stub.Pattern] = stub.KillCallerOffset
201-
}
202-
203194
s.Handle(items[0], items[1], func(req testserver.Request) any {
204195
if stub.Delay > 0 {
205196
ctx := req.Context
@@ -218,10 +209,6 @@ func startLocalServer(t *testing.T,
218209
}
219210
}
220211

221-
if shouldKillCaller(stub, offsetCounters, killCounters, killCountersMu) {
222-
killCaller(t, stub.Pattern, req.Headers)
223-
}
224-
225212
return stub.Response
226213
})
227214
}
@@ -232,50 +219,6 @@ func startLocalServer(t *testing.T,
232219
return s.URL
233220
}
234221

235-
func shouldKillCaller(stub ServerStub, offsetCounters, killCounters map[string]int, mu *sync.Mutex) bool {
236-
if stub.KillCaller <= 0 {
237-
return false
238-
}
239-
mu.Lock()
240-
defer mu.Unlock()
241-
242-
if offsetCounters[stub.Pattern] > 0 {
243-
offsetCounters[stub.Pattern]--
244-
return false
245-
}
246-
247-
if killCounters[stub.Pattern] <= 0 {
248-
return false
249-
}
250-
killCounters[stub.Pattern]--
251-
return true
252-
}
253-
254-
func killCaller(t *testing.T, pattern string, headers http.Header) {
255-
pid := testserver.ExtractPidFromHeaders(headers)
256-
if pid == 0 {
257-
t.Errorf("KillCaller configured but test-pid not found in User-Agent")
258-
return
259-
}
260-
261-
process, err := os.FindProcess(pid)
262-
if err != nil {
263-
t.Errorf("Failed to find process %d: %s", pid, err)
264-
return
265-
}
266-
267-
// Use process.Kill() for cross-platform compatibility.
268-
// On Unix, this sends SIGKILL. On Windows, this calls TerminateProcess.
269-
if err := process.Kill(); err != nil {
270-
t.Errorf("Failed to kill process %d: %s", pid, err)
271-
return
272-
}
273-
274-
if !waitForProcessExit(pid, 2*time.Second) {
275-
t.Logf("KillCaller: timed out waiting for PID %d to exit (pattern: %s)", pid, pattern)
276-
}
277-
t.Logf("KillCaller: killed PID %d (pattern: %s)", pid, pattern)
278-
}
279222

280223
func startProxyServer(t *testing.T,
281224
recordRequests bool,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
kill_after.py "GET /api/2.0/preview/scim/v2/Me" 0 1
12
trace errcode $CLI current-user me
23
echo "Script continued after kill"
Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1 @@
11
# Kill the CLI when it calls /Me endpoint (once, then allow)
2-
[[Server]]
3-
Pattern = "GET /api/2.0/preview/scim/v2/Me"
4-
KillCaller = 1

acceptance/selftest/kill_caller/multi_pattern/output.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ Me attempt 2 done
1313

1414
>>> [CLI] current-user me
1515
{
16-
"id": "123",
17-
"userName": "test@example.com"
16+
"id": "[USERID]",
17+
"userName": "[USERNAME]"
1818
}
1919
Me attempt 3 done - success!
2020

0 commit comments

Comments
 (0)