Skip to content

Commit 0fa2029

Browse files
committed
Integrate relay docker service tests and finalize subscribe timeout typing
1 parent 9901429 commit 0fa2029

15 files changed

Lines changed: 278 additions & 283 deletions

.github/workflows/ci.yml

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,6 @@ jobs:
6262
steps:
6363
- uses: actions/checkout@v6
6464

65-
- name: Check out pinned moq-dev relay workspace
66-
uses: actions/checkout@v6
67-
with:
68-
repository: moq-dev/moq
69-
ref: aae43f824b9b8d93b68dd6a765f8bec691f60c0b
70-
path: .moq-dev
71-
7265
- uses: erlef/setup-beam@v1
7366
with:
7467
elixir-version: '1.19.0'
@@ -94,16 +87,27 @@ jobs:
9487
~/.cargo/registry
9588
~/.cargo/git
9689
target
97-
.moq-dev/target
98-
key: ${{ runner.os }}-cargo-${{ hashFiles('mix.lock', 'native/moqx_native/Cargo.lock', '.moq-dev/Cargo.lock') }}
90+
key: ${{ runner.os }}-cargo-${{ hashFiles('mix.lock', 'native/moqx_native/Cargo.lock') }}
9991
restore-keys: |
10092
${{ runner.os }}-cargo-
10193
10294
- name: Install Elixir dependencies
10395
run: mix deps.get
10496

105-
- name: Build relay binary for integration tests
106-
run: cargo build --manifest-path .moq-dev/Cargo.toml --release -p moq-relay
97+
- name: Generate integration relay certificates
98+
run: scripts/generate_integration_certs.sh .tmp/integration-certs
99+
100+
- name: Start pinned relay image
101+
env:
102+
MOQX_RELAY_IMAGE: ghcr.io/moqtail/relay:sha-190e502@sha256:36c929b71140a83158da383721f1d59f199a9f643ab5d033910258f5aa2903ee
103+
run: docker compose -f docker-compose.integration.yml up -d relay
107104

108105
- name: Run relay-backed integration tests
106+
env:
107+
MOQX_EXTERNAL_RELAY_URL: https://localhost:4433
108+
MOQX_RELAY_CACERTFILE: .tmp/integration-certs/ca.pem
109109
run: mix test.integration
110+
111+
- name: Stop relay image
112+
if: always()
113+
run: docker compose -f docker-compose.integration.yml down --remove-orphans

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ moqx-*.tar
2727
/target/
2828
priv/native
2929
.moq-dev/
30+
.tmp

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
- before committing, ensure you `mix format`, run the tests, run `mix credo` (elixir's linter) and refactor accordingly, ensure documentation stays consistent
22
- if you find stale documentation, point it out: we either have to update or remove it
3+
- this project is based on moqtail. Before making any design decisions, ensure you check out what has been chosen there
4+
- this project is based on a series of MOQT IETF documents (linked in README), which should always be consulted before making decisions

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ All notable changes to `moqx` will be documented in this file.
4646
`await_catalog/2`).
4747
- Mix tasks (`moqx.e2e.pubsub`, `moqx.moqtail.demo`) and integration helpers now
4848
follow the typed async contract.
49+
- Integration tests now assume a relay endpoint provided by environment
50+
(`MOQX_EXTERNAL_RELAY_URL`) and trusted CA path (`MOQX_RELAY_CACERTFILE`),
51+
with Docker-based local/CI orchestration as the primary deterministic path.
52+
- Removed `:public_relay_live` integration tests; public relay interop checks now
53+
live in manual mix tasks.
54+
- Subscribe availability timeout is now exposed as
55+
`rendezvous_timeout_ms` (`delivery_timeout_ms` remains as a deprecated alias)
56+
and subscribe request rejections now carry typed `RequestError.code` values
57+
(for example `:track_does_not_exist` / `:timeout`).
4958

5059
### Documentation
5160

README.md

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,9 @@ Subscriptions are asynchronous and correlated by subscription handle.
227227

228228
`subscribe/4` options:
229229

230-
- `delivery_timeout_ms` -- MOQT DELIVERY TIMEOUT parameter (`0x02`) in milliseconds.
230+
- `rendezvous_timeout_ms` -- how long the relay may wait for publisher availability
231+
before rejecting subscribe (encoded as MOQT DELIVERY TIMEOUT parameter `0x02`).
232+
- `delivery_timeout_ms` -- deprecated alias for `rendezvous_timeout_ms`.
231233

232234
The subscription message contract is:
233235

@@ -240,7 +242,7 @@ The subscription message contract is:
240242
- `{:moqx_transport_error, %MOQX.TransportError{op: :subscribe, handle, ...}}`
241243

242244
```elixir
243-
{:ok, handle} = MOQX.subscribe(subscriber, "moqtail", "catalog", delivery_timeout_ms: 1_500)
245+
{:ok, handle} = MOQX.subscribe(subscriber, "moqtail", "catalog", rendezvous_timeout_ms: 1_500)
244246

245247
receive do
246248
{:moqx_subscribe_ok, %MOQX.SubscribeOk{handle: ^handle}} -> :ok
@@ -487,13 +489,29 @@ For an explicit split between fast checks and integration coverage:
487489

488490
```bash
489491
mix ci
490-
mix test.integration
492+
scripts/test_integration.sh
491493
```
492494

493495
- `mix ci` runs formatting, Credo, and non-integration tests
494-
- `mix test.integration` runs the deterministic integration suite (excludes `:public_relay_live` tests)
495-
- run live public-relay coverage explicitly with:
496-
`mix test --include integration --include public_relay_live`
496+
- `scripts/test_integration.sh` starts a pinned relay Docker image with local
497+
TLS certs and runs `mix test.integration`
498+
499+
You can override relay version independently from the locally compiled moqtail
500+
library by setting `MOQX_RELAY_IMAGE`, for example:
501+
502+
```bash
503+
MOQX_RELAY_IMAGE=ghcr.io/moqtail/relay:sha-190e502 scripts/test_integration.sh
504+
```
505+
506+
Set a digest-pinned reference for strict reproducibility:
507+
508+
```bash
509+
MOQX_RELAY_IMAGE='ghcr.io/moqtail/relay:sha-190e502@sha256:36c929b71140a83158da383721f1d59f199a9f643ab5d033910258f5aa2903ee' scripts/test_integration.sh
510+
```
511+
512+
`mix test.integration` can still be run directly if you provide a relay URL and
513+
trusted CA path via environment (`MOQX_EXTERNAL_RELAY_URL`,
514+
`MOQX_RELAY_CACERTFILE`).
497515

498516
### Local relay TLS
499517

docker-compose.integration.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
services:
2+
relay:
3+
image: ${MOQX_RELAY_IMAGE:-ghcr.io/moqtail/relay:sha-190e502@sha256:36c929b71140a83158da383721f1d59f199a9f643ab5d033910258f5aa2903ee}
4+
platform: linux/amd64
5+
command:
6+
- --host
7+
- 0.0.0.0
8+
- --port
9+
- "4433"
10+
- --cert-file
11+
- /certs/cert.pem
12+
- --key-file
13+
- /certs/key.pem
14+
ports:
15+
- "4433:4433/udp"
16+
volumes:
17+
- ./.tmp/integration-certs/cert.pem:/certs/cert.pem:ro
18+
- ./.tmp/integration-certs/key.pem:/certs/key.pem:ro

lib/moqx.ex

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,8 @@ defmodule MOQX do
163163

164164
@typedoc "Subscribe options accepted by `subscribe/4` and `subscribe_track/4`."
165165
@type subscribe_opt ::
166-
{:delivery_timeout_ms, non_neg_integer()}
166+
{:rendezvous_timeout_ms, non_neg_integer()}
167+
| {:delivery_timeout_ms, non_neg_integer()}
167168
| {:init_data, binary()}
168169
| {:track_meta, map()}
169170
| {:track, Track.t()}
@@ -693,7 +694,9 @@ defmodule MOQX do
693694
694695
Supported options:
695696
696-
- `:delivery_timeout_ms` -- MOQT DELIVERY TIMEOUT (parameter type `0x02`) in milliseconds
697+
- `:rendezvous_timeout_ms` -- how long the relay may wait for publisher availability
698+
before rejecting the request (encoded as MOQT DELIVERY TIMEOUT parameter `0x02`)
699+
- `:delivery_timeout_ms` -- deprecated alias for `:rendezvous_timeout_ms`
697700
- `:init_data` -- binary init segment/configuration to surface in `:moqx_track_init`
698701
- `:track_meta` -- map surfaced in `:moqx_track_init`
699702
- `:track` -- `%MOQX.Catalog.Track{}` convenience; fills `:init_data` and `:track_meta`
@@ -715,8 +718,7 @@ defmodule MOQX do
715718
{:ok, subscription_handle()} | {:error, MOQX.RequestError.t()}
716719
def subscribe(session, broadcast_path, track_name, opts)
717720
when is_binary(broadcast_path) and is_binary(track_name) and is_list(opts) do
718-
delivery_timeout_ms =
719-
opts |> Keyword.get(:delivery_timeout_ms) |> normalize_delivery_timeout_ms!()
721+
rendezvous_timeout_ms = normalize_rendezvous_timeout_ms!(opts)
720722

721723
{init_data, track_meta} = normalize_subscribe_track_payload!(opts)
722724

@@ -726,7 +728,7 @@ defmodule MOQX do
726728
session,
727729
broadcast_path,
728730
track_name,
729-
delivery_timeout_ms,
731+
rendezvous_timeout_ms,
730732
init_data,
731733
track_meta
732734
) do
@@ -790,7 +792,7 @@ defmodule MOQX do
790792
end
791793

792794
defp validate_subscribe_opts_keys!(opts) do
793-
allowed_keys = [:delivery_timeout_ms, :init_data, :track_meta, :track]
795+
allowed_keys = [:rendezvous_timeout_ms, :delivery_timeout_ms, :init_data, :track_meta, :track]
794796

795797
case Keyword.keys(opts) -- allowed_keys do
796798
[] -> :ok
@@ -843,15 +845,33 @@ defmodule MOQX do
843845
"expected :track_meta to be a map, got: #{inspect(track_meta)}"
844846
end
845847

846-
defp normalize_delivery_timeout_ms!(nil), do: nil
848+
defp normalize_rendezvous_timeout_ms!(opts) do
849+
rendezvous_timeout_ms = Keyword.get(opts, :rendezvous_timeout_ms)
850+
delivery_timeout_ms = Keyword.get(opts, :delivery_timeout_ms)
847851

848-
defp normalize_delivery_timeout_ms!(delivery_timeout_ms)
849-
when is_integer(delivery_timeout_ms) and delivery_timeout_ms >= 0,
850-
do: delivery_timeout_ms
852+
case {rendezvous_timeout_ms, delivery_timeout_ms} do
853+
{nil, nil} ->
854+
nil
851855

852-
defp normalize_delivery_timeout_ms!(delivery_timeout_ms) do
856+
{timeout_ms, nil} ->
857+
normalize_timeout_ms!(timeout_ms, :rendezvous_timeout_ms)
858+
859+
{nil, timeout_ms} ->
860+
normalize_timeout_ms!(timeout_ms, :delivery_timeout_ms)
861+
862+
{_rendezvous, _delivery} ->
863+
raise ArgumentError,
864+
"subscribe/4 accepts only one of :rendezvous_timeout_ms or :delivery_timeout_ms"
865+
end
866+
end
867+
868+
defp normalize_timeout_ms!(timeout_ms, _key)
869+
when is_integer(timeout_ms) and timeout_ms >= 0,
870+
do: timeout_ms
871+
872+
defp normalize_timeout_ms!(timeout_ms, key) do
853873
raise ArgumentError,
854-
"expected :delivery_timeout_ms to be a non-negative integer, got: #{inspect(delivery_timeout_ms)}"
874+
"expected #{inspect(key)} to be a non-negative integer, got: #{inspect(timeout_ms)}"
855875
end
856876

857877
# ---------------------------------------------------------------------------

lib/moqx/native.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ defmodule MOQX.Native do
4141
_session,
4242
_broadcast_path,
4343
_track_name,
44-
_delivery_timeout_ms,
44+
_rendezvous_timeout_ms,
4545
_init_data,
4646
_track_meta
4747
),

mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ defmodule MOQX.MixProject do
6868
defp aliases do
6969
[
7070
ci: ["format --check-formatted", "credo --strict", "test --exclude integration"],
71-
"test.integration": ["test --only integration --exclude public_relay_live"]
71+
"test.integration": ["test --only integration"]
7272
]
7373
end
7474
end

native/moqx_native/src/lib.rs

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use moqtail::model::common::pair::KeyValuePair;
1616
use moqtail::model::common::tuple::{Tuple, TupleField};
1717
use moqtail::model::control::client_setup::ClientSetup;
1818
use moqtail::model::control::constant::{
19-
GroupOrder as MoqGroupOrder, PublishDoneStatusCode, DRAFT_14,
19+
GroupOrder as MoqGroupOrder, PublishDoneStatusCode, SubscribeErrorCode, DRAFT_14,
2020
};
2121
use moqtail::model::control::control_message::ControlMessage;
2222
use moqtail::model::control::fetch::{Fetch as MoqFetch, StandAloneFetchProps};
@@ -73,6 +73,14 @@ mod atoms {
7373
does_not_exist,
7474
end_of_group,
7575
end_of_track,
76+
internal_error,
77+
unauthorized,
78+
timeout,
79+
not_supported,
80+
track_does_not_exist,
81+
invalid_range,
82+
malformed_auth_token,
83+
expired_auth_token,
7684
}
7785
}
7886

@@ -229,6 +237,19 @@ fn publish_done_status_to_atom(status: PublishDoneStatusCode) -> Atom {
229237
}
230238
}
231239

240+
fn subscribe_error_code_to_atom(code: SubscribeErrorCode) -> Atom {
241+
match code {
242+
SubscribeErrorCode::InternalError => atoms::internal_error(),
243+
SubscribeErrorCode::Unauthorized => atoms::unauthorized(),
244+
SubscribeErrorCode::Timeout => atoms::timeout(),
245+
SubscribeErrorCode::NotSupported => atoms::not_supported(),
246+
SubscribeErrorCode::TrackDoesNotExist => atoms::track_does_not_exist(),
247+
SubscribeErrorCode::InvalidRange => atoms::invalid_range(),
248+
SubscribeErrorCode::MalformedAuthToken => atoms::malformed_auth_token(),
249+
SubscribeErrorCode::ExpiredAuthToken => atoms::expired_auth_token(),
250+
}
251+
}
252+
232253
fn runtime() -> &'static Runtime {
233254
static RT: OnceLock<Runtime> = OnceLock::new();
234255
RT.get_or_init(|| {
@@ -1003,7 +1024,7 @@ fn dispatch_control_response(msg: ControlMessage, inner: &Arc<SessionInner>) {
10031024
let payload = RequestErrorOut {
10041025
op: atoms::subscribe(),
10051026
message: reason.clone(),
1006-
code: None,
1027+
code: Some(subscribe_error_code_to_atom(err.error_code)),
10071028
r#ref: nil,
10081029
handle: sub_ref.in_env(env),
10091030
};
@@ -2470,7 +2491,7 @@ fn subscribe<'a>(
24702491
session: ResourceArc<SessionRes>,
24712492
broadcast_path: String,
24722493
track_name: String,
2473-
delivery_timeout_ms: Option<u64>,
2494+
rendezvous_timeout_ms: Option<u64>,
24742495
init_data: Option<Binary>,
24752496
track_meta: rustler::Term,
24762497
) -> rustler::NifResult<rustler::Term<'a>> {
@@ -2484,11 +2505,11 @@ fn subscribe<'a>(
24842505
let inner = session.inner.clone();
24852506
let namespace = normalize_path(&session.root, &broadcast_path);
24862507

2487-
let subscribe_parameters = match delivery_timeout_ms {
2508+
let subscribe_parameters = match rendezvous_timeout_ms {
24882509
Some(timeout_ms) => {
24892510
vec![KeyValuePair::try_new_varint(0x02, timeout_ms).map_err(|e| {
24902511
rustler::Error::Term(Box::new(format!(
2491-
"invalid delivery_timeout_ms parameter: {:?}",
2512+
"invalid rendezvous timeout parameter: {:?}",
24922513
e
24932514
)))
24942515
})?]

0 commit comments

Comments
 (0)