Skip to content

Commit 49d10ef

Browse files
committed
client: support incremental multicast group subscription
Remove the guard in the CLI that blocked `doublezero connect multicast` when a multicast service was already running. The onchain subscribe instruction, CLI's find_or_create_user_and_subscribe, and daemon's InfraEqual + UpdateGroups path already support incremental group additions without tearing down the tunnel. Also improve e2e test ergonomics: - Add Make targets: test-debug, test-nobuild, test-keep, test-cleanup - Rename DZ_E2E_DEBUG env var to DEBUG for consistency with shreds repo - Update dev/e2e-test.sh and dev/e2e-until-fail.sh to use Make - Update CLAUDE.md and DEVELOPMENT.md with new Make target usage
1 parent bdb6542 commit 49d10ef

11 files changed

Lines changed: 165 additions & 75 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file.
99
### Changes
1010

1111
- CLI
12+
- Allow incremental multicast group addition without disconnecting
1213
- Reset SIGPIPE to SIG_DFL at the start of main() in all 3 CLI binaries (doublezero, doublezero-geolocation, doublezero-admin) so the process exits silently like standard CLI tools
1314
- SDK
1415
- Add Go SDK for shred subscription program with read-only account deserialization (epoch state, seat assignments, pricing, settlement, validator client rewards), PDA derivation helpers, RPC fetchers, compatibility tests, and a fetch example CLI

CLAUDE.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -257,11 +257,20 @@ Only use `dev/dzctl destroy -y` when you need a completely clean slate (e.g., le
257257

258258
```bash
259259
# Run a specific test (preferred)
260-
go test -tags e2e -run TestE2E_Multicast_Publisher -v -count=1 ./e2e/...
260+
make e2e-test RUN=TestE2E_Multicast_Publisher
261261

262-
# Run all tests (requires high-memory machine)
263-
dev/e2e-test.sh
262+
# Run with debug logging
263+
make e2e-test-debug RUN=TestE2E_Multicast_Publisher
264+
265+
# Skip docker image rebuild
266+
make e2e-test-nobuild RUN=TestE2E_Multicast_Publisher
264267

265268
# Keep containers after test completion/failure for debugging
266-
TESTCONTAINERS_RYUK_DISABLED=true go test -tags e2e -run TestE2E_Multicast_Publisher -v -count=1 ./e2e/...
269+
make e2e-test-keep RUN=TestE2E_Multicast_Publisher
270+
271+
# Run all tests (requires high-memory machine)
272+
make e2e-test
273+
274+
# Clean up leftover containers
275+
make e2e-test-cleanup
267276
```

DEVELOPMENT.md

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,23 +55,31 @@ The required image (`ghcr.io/malbeclabs/ceos:4.33.1F`) will be pulled automatica
5555
End-to-end tests exercise the full DoubleZero stack — smartcontracts, controller, activator, client, and device agents — all running in isolated Docker containers.
5656

5757
```bash
58-
# Run a specific E2E test directly
59-
cd e2e/
60-
go test -tags e2e -v -run TestE2E_MultiClient
58+
# Run a specific test
59+
make e2e-test RUN=TestE2E_MultiClient
6160
62-
# Or use the helper script
63-
dev/e2e-test.sh TestE2E_MultiClient
61+
# Run with debug logging
62+
make e2e-test-debug RUN=TestE2E_MultiClient
63+
64+
# Skip docker image rebuild (faster iteration)
65+
make e2e-test-nobuild RUN=TestE2E_MultiClient
66+
67+
# Keep containers after test for debugging
68+
make e2e-test-keep RUN=TestE2E_MultiClient
69+
70+
# Both: skip rebuild + keep containers
71+
make e2e-test-keep-nobuild RUN=TestE2E_MultiClient
72+
73+
# Clean up leftover containers from previous runs
74+
make e2e-test-cleanup
75+
76+
# Run all tests (requires high-memory machine)
77+
make e2e-test
6478
```
6579

6680
> ⚠️ Note:
6781
>
68-
>
69-
> E2E tests are resource-intensive. It’s recommended to run them individually or with low parallelism:
70-
>
71-
> ```bash
72-
> go test -tags e2e -v -parallel=1 -timeout=20m
73-
> ```
74-
>
82+
> E2E tests are resource-intensive. It’s recommended to run them individually.
7583
> Running all tests together may require at least 64 GB of memory available to Docker.
7684
>
7785

Makefile

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -198,15 +198,48 @@ generate-fixtures:
198198

199199
# -----------------------------------------------------------------------------
200200
# E2E targets
201+
#
202+
# Usage:
203+
# make e2e-test # run all tests
204+
# make e2e-test RUN=TestE2E_Multicast # run a specific test
205+
# make e2e-test-debug RUN=TestE2E_Multicast # with debug logging
206+
# make e2e-test-nobuild # skip docker image build
207+
# make e2e-test-keep # keep containers after test
208+
# make e2e-test-keep-nobuild # both
209+
# make e2e-test-cleanup # remove leftover containers
201210
# -----------------------------------------------------------------------------
202-
.PHONY: e2e-test
203-
e2e-test:
204-
cd e2e && $(MAKE) test
205-
206211
.PHONY: e2e-build
207212
e2e-build:
208213
cd e2e && $(MAKE) build
209214

215+
.PHONY: e2e-build-debug
216+
e2e-build-debug:
217+
cd e2e && $(MAKE) build-debug
218+
219+
.PHONY: e2e-test
220+
e2e-test:
221+
cd e2e && $(MAKE) test $(if $(RUN),RUN=$(RUN))
222+
223+
.PHONY: e2e-test-debug
224+
e2e-test-debug:
225+
cd e2e && $(MAKE) test-debug $(if $(RUN),RUN=$(RUN))
226+
227+
.PHONY: e2e-test-nobuild
228+
e2e-test-nobuild:
229+
cd e2e && $(MAKE) test-nobuild $(if $(RUN),RUN=$(RUN))
230+
231+
.PHONY: e2e-test-keep
232+
e2e-test-keep:
233+
cd e2e && $(MAKE) test-keep $(if $(RUN),RUN=$(RUN))
234+
235+
.PHONY: e2e-test-keep-nobuild
236+
e2e-test-keep-nobuild:
237+
cd e2e && $(MAKE) test-keep-nobuild $(if $(RUN),RUN=$(RUN))
238+
239+
.PHONY: e2e-test-cleanup
240+
e2e-test-cleanup:
241+
cd e2e && $(MAKE) test-cleanup
242+
210243
# -----------------------------------------------------------------------------
211244
# Build programs for specific environments
212245
# -----------------------------------------------------------------------------

client/doublezero/src/command/connect.rs

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -204,23 +204,6 @@ impl ProvisioningCliCommand {
204204
client_ip: Ipv4Addr,
205205
spinner: &ProgressBar,
206206
) -> eyre::Result<()> {
207-
// Check if the daemon already has a multicast service running. The daemon
208-
// does not support updating an existing service — both publisher and subscriber
209-
// roles must be specified in a single connect command.
210-
if let Ok(statuses) = controller.status().await {
211-
if statuses.iter().any(|s| {
212-
s.user_type
213-
.as_ref()
214-
.is_some_and(|t| t.eq_ignore_ascii_case("multicast"))
215-
}) {
216-
eyre::bail!(
217-
"A multicast service is already running. Disconnect first with \
218-
`doublezero disconnect multicast`, then reconnect with all desired \
219-
groups in a single command (e.g. --publish and --subscribe)."
220-
);
221-
}
222-
}
223-
224207
let mcast_groups = client.list_multicastgroup(ListMulticastGroupCommand)?;
225208

226209
// Resolve pub group codes to pubkeys

dev/e2e-test.sh

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@ workspace_dir=$(dirname "${script_dir}")
66

77
test=${1:-}
88

9-
cd "${workspace_dir}/e2e"
10-
119
if [ -n "${test}" ]; then
12-
go test -v -tags e2e -run="${test}" -timeout 20m
10+
make -C "${workspace_dir}/e2e" test RUN="${test}"
1311
else
14-
make test verbose
12+
make -C "${workspace_dir}/e2e" test
1513
fi

dev/e2e-until-fail.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ while :; do
5757
fi
5858

5959
set +e
60-
"${workspace_dir}/dev/e2e-test.sh" "${target_test}"
60+
make -C "${workspace_dir}/e2e" test-nobuild $(if [ -n "$target_test" ]; then echo "RUN=$target_test"; fi)
6161
ret_val=$?
6262
set -e
6363

e2e/Makefile

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,55 @@ GIT_SHA:=`git rev-parse --short HEAD`
66
# Enabled by default on mac, but not always on linux.
77
export DOCKER_BUILDKIT=1
88

9+
.PHONY: build build-debug test test-debug test-nobuild test-keep test-keep-nobuild test-cleanup
10+
11+
# -----------------------------------------------------------------------------
12+
# Build
913
# -----------------------------------------------------------------------------
10-
# Run the e2e tests.
14+
build:
15+
go run ./cmd/dzctl/main.go build
16+
17+
build-debug:
18+
go run ./cmd/dzctl/main.go build -v
19+
20+
# -----------------------------------------------------------------------------
21+
# Test
1122
#
1223
# This will build the docker images first, so it's not necessary to run `build`
1324
# before running `test`.
1425
#
1526
# We configure -timeout=20m for the case where the user is running the tests
1627
# sequentially. This should be more than enough time for the tests to run in
1728
# that case, and leave room in case more tests are added in the future.
29+
#
30+
# Usage:
31+
# make test # run all tests
32+
# make test RUN=TestE2E_Multicast # run a specific test
33+
# make test-debug RUN=TestE2E_Multicast # with debug logging
34+
# make test-nobuild # skip docker image build
35+
# make test-keep # keep containers after test
36+
# make test-keep-nobuild # both
1837
# -----------------------------------------------------------------------------
19-
.PHONY: test
2038
test:
21-
$(if $(findstring nobuild,$(MAKECMDGOALS)),DZ_E2E_NO_BUILD=1) go test -tags=e2e -timeout=20m $(if $(parallel),-parallel=$(parallel)) $(if $(run),-run=$(run)) $(if $(findstring verbose,$(MAKECMDGOALS)),-v)
39+
go test -tags=e2e -timeout=20m -v -count=1 $(if $(RUN),-run $(RUN)) .
2240

23-
# Dummy target to suppress errors when using 'nobuild' as a flag in `make test nobuild
24-
.PHONY: nobuild
25-
nobuild:
26-
@:
41+
test-debug:
42+
DEBUG=1 go test -tags=e2e -timeout=20m -v -count=1 $(if $(RUN),-run $(RUN)) .
2743

28-
# Dummy target to suppress errors when using 'verbose' as a flag in `make test verbose`
29-
.PHONY: verbose
30-
verbose:
31-
@:
44+
test-nobuild:
45+
DZ_E2E_NO_BUILD=1 go test -tags=e2e -timeout=20m -v -count=1 $(if $(RUN),-run $(RUN)) .
3246

33-
.PHONY: build
34-
build:
35-
go run ./cmd/dzctl/main.go build
47+
test-keep:
48+
TESTCONTAINERS_RYUK_DISABLED=true go test -tags=e2e -timeout=20m -v -count=1 $(if $(RUN),-run $(RUN)) .
49+
50+
test-keep-nobuild:
51+
TESTCONTAINERS_RYUK_DISABLED=true DZ_E2E_NO_BUILD=1 go test -tags=e2e -timeout=20m -v -count=1 $(if $(RUN),-run $(RUN)) .
3652

53+
test-cleanup:
54+
@echo "Removing containers with label dz.malbeclabs.com..."
55+
@docker rm -f $$(docker ps -aq --filter label=dz.malbeclabs.com) 2>/dev/null || true
56+
@echo "Removing networks with label dz.malbeclabs.com..."
57+
@docker network rm $$(docker network ls -q --filter label=dz.malbeclabs.com) 2>/dev/null || true
3758

3859
# -----------------------------------------------------------------------------
3960
# Solana image build and push.

e2e/main_test.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ func TestMain(m *testing.M) {
7676
if vFlag := flag.Lookup("test.v"); vFlag != nil && vFlag.Value.String() == "true" {
7777
verbose = true
7878
}
79-
if os.Getenv("DZ_E2E_DEBUG") != "" {
79+
if os.Getenv("DEBUG") != "" {
8080
debug = true
8181
}
8282

@@ -534,6 +534,30 @@ func (dn *TestDevnet) ConnectMulticastSubscriberSkipAccessPass(t *testing.T, cli
534534
dn.log.Debug("--> Multicast subscriber connected")
535535
}
536536

537+
// AddMulticastPublisherGroupSkipAccessPass incrementally adds publish groups to
538+
// an existing multicast service without disconnecting.
539+
func (dn *TestDevnet) AddMulticastPublisherGroupSkipAccessPass(t *testing.T, client *devnet.Client, multicastGroupCodes ...string) {
540+
dn.log.Debug("==> Adding multicast publisher groups incrementally", "clientIP", client.CYOANetworkIP, "groups", multicastGroupCodes)
541+
542+
groupArgs := strings.Join(multicastGroupCodes, " ")
543+
_, err := client.Exec(t.Context(), []string{"bash", "-c", "doublezero connect multicast --publish " + groupArgs})
544+
require.NoError(t, err)
545+
546+
dn.log.Debug("--> Multicast publisher groups added incrementally")
547+
}
548+
549+
// AddMulticastSubscriberGroupSkipAccessPass incrementally adds subscribe groups to
550+
// an existing multicast service without disconnecting.
551+
func (dn *TestDevnet) AddMulticastSubscriberGroupSkipAccessPass(t *testing.T, client *devnet.Client, multicastGroupCodes ...string) {
552+
dn.log.Debug("==> Adding multicast subscriber groups incrementally", "clientIP", client.CYOANetworkIP, "groups", multicastGroupCodes)
553+
554+
groupArgs := strings.Join(multicastGroupCodes, " ")
555+
_, err := client.Exec(t.Context(), []string{"bash", "-c", "doublezero connect multicast --subscribe " + groupArgs})
556+
require.NoError(t, err)
557+
558+
dn.log.Debug("--> Multicast subscriber groups added incrementally")
559+
}
560+
537561
func (dn *TestDevnet) DisconnectMulticastSubscriber(t *testing.T, client *devnet.Client) {
538562
dn.log.Debug("==> Disconnecting multicast subscriber", "clientIP", client.CYOANetworkIP)
539563

@@ -765,7 +789,7 @@ func (dn *TestDevnet) BuildAgentConfigData(t *testing.T, deviceCode string, extr
765789

766790
// newTestLoggerForTest creates a logger for individual test runs.
767791
// Logs are written to t.Log() so they only appear on test failure (unless -v is passed).
768-
// With DZ_E2E_DEBUG=1, shows DEBUG level logs; otherwise shows INFO level.
792+
// With DEBUG=1, shows DEBUG level logs; otherwise shows INFO level.
769793
func newTestLoggerForTest(t *testing.T) *slog.Logger {
770794
w := &testWriter{t: t}
771795
logLevel := slog.LevelInfo

e2e/multicast_test.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -192,13 +192,23 @@ func TestE2E_Multicast(t *testing.T) {
192192
createMulticastGroupForBothClients(t, tdn, publisherClient, subscriberClient, "mg02")
193193

194194
if !t.Run("connect", func(t *testing.T) {
195-
// Connect publisher.
196-
tdn.ConnectMulticastPublisherSkipAccessPass(t, publisherClient, "mg01", "mg02")
195+
// Connect publisher to first group only.
196+
tdn.ConnectMulticastPublisherSkipAccessPass(t, publisherClient, "mg01")
197197
err = publisherClient.WaitForTunnelUp(t.Context(), 90*time.Second)
198198
require.NoError(t, err)
199199

200-
// Connect subscriber.
201-
tdn.ConnectMulticastSubscriberSkipAccessPass(t, subscriberClient, "mg01", "mg02")
200+
// Incrementally add second publish group without disconnecting.
201+
tdn.AddMulticastPublisherGroupSkipAccessPass(t, publisherClient, "mg02")
202+
err = publisherClient.WaitForTunnelUp(t.Context(), 90*time.Second)
203+
require.NoError(t, err)
204+
205+
// Connect subscriber to first group only.
206+
tdn.ConnectMulticastSubscriberSkipAccessPass(t, subscriberClient, "mg01")
207+
err = subscriberClient.WaitForTunnelUp(t.Context(), 90*time.Second)
208+
require.NoError(t, err)
209+
210+
// Incrementally add second subscribe group without disconnecting.
211+
tdn.AddMulticastSubscriberGroupSkipAccessPass(t, subscriberClient, "mg02")
202212
err = subscriberClient.WaitForTunnelUp(t.Context(), 90*time.Second)
203213
require.NoError(t, err)
204214

0 commit comments

Comments
 (0)