Skip to content

Commit c74b30a

Browse files
authored
Merge branch 'master' into test-skip-unity-external
2 parents 7d9fa86 + 79137b5 commit c74b30a

5 files changed

Lines changed: 147 additions & 52 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,7 @@ jobs:
6161
if: runner.os == 'Windows'
6262
- name: Run smoketests
6363
# Note: clear_database and replication only work in private
64-
# zz_docker disabled temporarily until https://github.com/clockworklabs/SpacetimeDB/issues/2965
65-
run: python -m smoketests ${{ matrix.smoketest_args }} -x clear_database replication zz_docker
64+
run: python -m smoketests ${{ matrix.smoketest_args }} -x clear_database replication
6665
- name: Stop containers (Linux)
6766
if: always() && runner.os == 'Linux'
6867
run: docker compose down

crates/paths/src/cli.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ impl VersionBinDir {
8585
}
8686

8787
fn link_to(&self, path: &Path) -> anyhow::Result<()> {
88-
let rel_path = path.strip_prefix(self).unwrap_or(path);
88+
let rel_path = path.strip_prefix(self.0.parent().unwrap()).unwrap_or(path);
8989
#[cfg(unix)]
9090
{
9191
// remove the link if it already exists

crates/update/src/cli/list.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use spacetimedb_paths::SpacetimePaths;
2+
use std::path::Path;
23

34
/// List installed SpacetimeDB versions.
45
#[derive(clap::Args)]
@@ -10,7 +11,31 @@ pub(super) struct List {
1011

1112
impl List {
1213
pub(super) fn exec(self, paths: &SpacetimePaths) -> anyhow::Result<()> {
13-
let current = paths.cli_bin_dir.current_version()?;
14+
// This `match` part is only here because at one point we had a bug where we were creating
15+
// symlinks that contained the _entire_ path, rather than just the relative path to the
16+
// version directory. It's not strictly necessary, it just fixes our determination of what
17+
// the current version is, for the output of this command.
18+
//
19+
// That symlink bug was fixed in `crates/paths/src/cli.rs` in
20+
// https://github.com/clockworklabs/SpacetimeDB/pull/2680, but this `match` means that this
21+
// output will still be correct for any users that already have one of the bugged symlinks.
22+
//
23+
// Once users upgrade to a version containing #2680, they will have the code that creates
24+
// the fixed symlinks. However, that code won't immediately run, since the upgrade will be
25+
// running from the previous binary they had. So once they upgrade to a version containing
26+
// #2680, _and then_ upgrade once more, their symlinks will be fixed. There's no real
27+
// timeline on when everyone will have done that, but hopefully that helps give a sense of
28+
// how long this code "should" exist for (but it doesn't do any harm afaik).
29+
let current = match paths.cli_bin_dir.current_version()? {
30+
None => None,
31+
Some(path_str) => {
32+
let file_name = Path::new(&path_str)
33+
.file_name()
34+
.and_then(|f| f.to_str())
35+
.ok_or(anyhow::anyhow!("Could not extract current version"))?;
36+
Some(file_name.to_string())
37+
}
38+
};
1439
let versions = if self.all {
1540
let client = super::reqwest_client()?;
1641
super::tokio_block_on(super::install::available_releases(&client))??

docker-compose.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
version: "3.6"
2-
31
services:
42
node:
3+
labels:
4+
app: spacetimedb
55
build:
66
context: ./
77
dockerfile: ./crates/standalone/Dockerfile
@@ -28,8 +28,6 @@ services:
2828
# Tracy
2929
- "8086:8086"
3030
entrypoint: cargo watch -i flamegraphs -i log.conf --why -C crates/standalone -x 'run start --data-dir=/stdb/data --jwt-pub-key-path=/etc/spacetimedb/id_ecdsa.pub --jwt-priv-key-path=/etc/spacetimedb/id_ecdsa'
31-
healthcheck:
32-
test: curl -f http://localhost/ping || exit 1
3331
privileged: true
3432
environment:
3533
SPACETIMEDB_FLAMEGRAPH_PATH: ../../../../flamegraphs/flamegraph.folded

smoketests/docker.py

Lines changed: 117 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,95 @@
1-
from dataclasses import dataclass
1+
import json
22
import os
33
import subprocess
44
import time
5-
from typing import List, Optional
5+
from dataclasses import dataclass
6+
from typing import List, Optional, Callable
67
from urllib.request import urlopen
8+
79
from . import COMPOSE_FILE
8-
import json
10+
911

1012
def restart_docker():
13+
"""
14+
Restart all containers defined in the current `COMPOSE_FILE`.
15+
16+
Checks that all spacetimedb containers are up and running after the restart.
17+
If they're not up after a couple of retries, throws an `Exception`.
18+
"""
19+
print("Restarting containers")
20+
1121
docker = DockerManager(COMPOSE_FILE)
12-
# Restart all containers.
1322
docker.compose("restart")
14-
# Ensure all nodes are reachable from outside.
15-
containers = docker.list_containers()
16-
for container in containers:
17-
info = json.loads(docker._execute_command("docker", "inspect", container.name))
18-
try:
19-
port = info[0]['NetworkSettings']['Ports']['80/tcp'][0]['HostPort']
20-
except KeyError:
21-
continue
22-
ping("127.0.0.1:{}".format(port))
23-
# TODO: ping endpoint needs to wait for database startup & leader election
24-
time.sleep(2)
25-
26-
def ping(host):
27-
tries = 0
28-
while tries < 10:
29-
tries += 1
30-
try:
31-
print(f"Ping Server at {host}")
32-
urlopen(f"http://{host}/v1/ping")
33-
print(f"Server up after {tries} tries")
34-
break
35-
except Exception:
36-
print("Server down")
37-
time.sleep(3)
38-
else:
39-
raise Exception(f"Server at {host} not responding")
23+
containers = docker.list_spacetimedb_containers()
24+
if not containers:
25+
raise Exception("No spacetimedb containers found")
26+
27+
# Ensure all nodes are running.
28+
attempts = 0
29+
while attempts < 5:
30+
attempts += 1
31+
if all(container.is_running(docker, spacetimedb_ping_url) for container in containers):
32+
# sleep a bit more to allow for leader election etc
33+
# TODO: make ping endpoint consider all server state
34+
time.sleep(2)
35+
return
36+
else:
37+
time.sleep(1)
38+
39+
raise Exception("Not all containers are up and running")
40+
41+
def spacetimedb_ping_url(port: int) -> str:
42+
return f"http://127.0.0.1:{port}/v1/ping"
4043

4144
@dataclass
4245
class DockerContainer:
4346
"""Represents a Docker container with its basic properties."""
4447
id: str
4548
name: str
4649

50+
def host_ports(self, docker) -> set[int]:
51+
"""
52+
Collect all host ports of this container.
53+
54+
Host ports are ports on the host that are bound to ports of the
55+
container.
56+
If the container is not currently running, an empty set is returned.
57+
"""
58+
host_ports = set()
59+
info = docker.inspect_container(self)
60+
for ports in info['NetworkSettings']['Ports'].values():
61+
for ip_and_port in ports:
62+
host_port = ip_and_port.get("HostPort")
63+
if host_port:
64+
host_ports.add(host_port)
65+
return host_ports
66+
67+
def is_running(self, docker, ping_url: Callable[[int], str]) -> bool:
68+
"""
69+
Check if the container is running.
70+
71+
`ping_url` takes a port number and returns a URL string that can be used
72+
to determine if the host is running by returning a 200 status.
73+
74+
If `self.host_ports()` returns a non-empty set, and one `ping_url`
75+
request is successful, the container is considered running.
76+
"""
77+
host_ports = self.host_ports(docker)
78+
for port in host_ports:
79+
url = ping_url(port)
80+
print(f"Trying {url} ... ", end='', flush=True)
81+
try:
82+
with urlopen(url, timeout=0.2) as response:
83+
if response.status == 200:
84+
print("ok")
85+
return True
86+
except Exception as e:
87+
print(f"error: {e}")
88+
continue
89+
90+
print(f"container {self.name} not running")
91+
return False
92+
4793
class DockerManager:
4894
"""Manages all Docker and Docker Compose operations."""
4995

@@ -74,19 +120,52 @@ def _execute_command(self, *args: str) -> str:
74120
raise
75121

76122
def compose(self, *args: str) -> str:
77-
"""Execute a docker-compose command."""
123+
"""Execute a `docker compose` command."""
78124
return self._execute_command("docker", "compose", "-f", self.compose_file, *args)
79125

80-
def list_containers(self) -> List[DockerContainer]:
81-
"""List all containers and return as DockerContainer objects."""
82-
output = self.compose("ps", "-a", "--format", "{{.ID}} {{.Name}}")
126+
def docker(self, *args: str) -> str:
127+
"""Execute a `docker` command."""
128+
return self._execute_command("docker", *args)
129+
130+
def list_containers(self, *filters) -> List[DockerContainer]:
131+
"""
132+
List the containers of the current compose file and return as DockerContainer objects.
133+
134+
All containers are considered, even if not running ('-a' flag).
135+
The containers may be filtered by 'filters' ('--filter' option).
136+
"""
137+
# Use -a so we don't miss a crashed or killed container
138+
# when checking for readiness.
139+
cmd = ["ps", "-a"]
140+
141+
# Restrict to the current compose file.
142+
compose_file = os.path.abspath(COMPOSE_FILE)
143+
cmd.extend(["--filter", f"label=com.docker.compose.project.config_files={compose_file}"])
144+
145+
# Apply additional filters.
146+
for f in filters:
147+
cmd.extend(["--filter", f])
148+
149+
# Output only the fields we need for `DockerContainer`.
150+
cmd.extend(["--format", "{{.ID}} {{.Names}}"])
151+
152+
output = self.docker(*cmd)
83153
containers = []
84154
for line in output.splitlines():
85155
if line.strip():
86156
container_id, name = line.split(maxsplit=1)
87157
containers.append(DockerContainer(id=container_id, name=name))
88158
return containers
89159

160+
def list_spacetimedb_containers(self) -> List[DockerContainer]:
161+
"""List all containers running spacetimedb."""
162+
return self.list_containers("label=app=spacetimedb")
163+
164+
def inspect_container(self, container: DockerContainer):
165+
"""Run the `inspect` command for `container`, returning the parsed JSON dict."""
166+
info = self.docker("inspect", container.name)
167+
return json.loads(info)[0]
168+
90169
def get_container_by_name(self, name: str) -> Optional[DockerContainer]:
91170
"""Find a container by name pattern."""
92171
return next(
@@ -97,29 +176,23 @@ def get_container_by_name(self, name: str) -> Optional[DockerContainer]:
97176
def kill_container(self, container_id: str):
98177
"""Kill a container by ID."""
99178
print(f"Killing container {container_id}")
100-
self._execute_command("docker", "kill", container_id)
179+
self.docker("kill", container_id)
101180

102181
def start_container(self, container_id: str):
103182
"""Start a container by ID."""
104183
print(f"Starting container {container_id}")
105-
self._execute_command("docker", "start", container_id)
184+
self.docker("start", container_id)
106185

107186
def disconnect_container(self, container_id: str):
108187
"""Disconnect a container from the network."""
109188
print(f"Disconnecting container {container_id}")
110-
self._execute_command(
111-
"docker", "network", "disconnect",
112-
self.network_name, container_id
113-
)
189+
self.docker("network", "disconnect", self.network_name, container_id)
114190
print(f"Disconnected container {container_id}")
115191

116192
def connect_container(self, container_id: str):
117193
"""Connect a container to the network."""
118194
print(f"Connecting container {container_id}")
119-
self._execute_command(
120-
"docker", "network", "connect",
121-
self.network_name, container_id
122-
)
195+
self.docker("network", "connect", self.network_name, container_id)
123196
print(f"Connected container {container_id}")
124197

125198
def generate_root_token(self) -> str:

0 commit comments

Comments
 (0)