This is an automated email from the ASF dual-hosted git repository.
kparisa pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iggy-website.git
The following commit(s) were added to refs/heads/main by this push:
new d59159783 docs: add a page on testing with Iggy (#120)
d59159783 is described below
commit d591597831046286c93a7b55ce3e3a45aef1070a
Author: Justin Mclean <[email protected]>
AuthorDate: Tue Oct 6 17:35:59 2026 +1100
docs: add a page on testing with Iggy (#120)
Shows how to run the Iggy Docker image from a test suite with
testcontainers, with examples for Rust, Python and Node.js.
Co-authored-by: Justin Mclean <[email protected]>
Co-authored-by: Kranti Parisa <[email protected]>
---
content/docs/server/meta.json | 2 +-
content/docs/server/testing.mdx | 293 ++++++++++++++++++++++++++++++++++++++++
2 files changed, 294 insertions(+), 1 deletion(-)
diff --git a/content/docs/server/meta.json b/content/docs/server/meta.json
index a80c2e9d8..b6816b590 100644
--- a/content/docs/server/meta.json
+++ b/content/docs/server/meta.json
@@ -1,4 +1,4 @@
{
"title": "Server",
- "pages": ["introduction", "configuration", "topic-options",
"storage-engine", "durability", "networking", "security", "docker",
"linux-tuning", "benchmarking"]
+ "pages": ["introduction", "configuration", "topic-options",
"storage-engine", "durability", "networking", "security", "docker", "testing",
"linux-tuning", "benchmarking"]
}
diff --git a/content/docs/server/testing.mdx b/content/docs/server/testing.mdx
new file mode 100644
index 000000000..6a769c81f
--- /dev/null
+++ b/content/docs/server/testing.mdx
@@ -0,0 +1,293 @@
+---
+title: Testing with Iggy
+description: "Run the Iggy Docker image from your own test suite, with
examples for Rust, Python and Node.js."
+---
+
+Iggy has no embedded, in-process test server. The server ships as a binary,
not a library, so a test suite can't start it in-process. To test against a
real server, start the published Docker image when your tests start and stop it
when they finish. The examples below use each language's `testcontainers`
library to do that.
+
+## What the container needs
+
+These settings apply whichever language your tests are in:
+
+- **`seccomp=unconfined`** as a security option. Docker's default seccomp
profile blocks the `io_uring` calls the server needs.
+- **`IGGY_ROOT_USERNAME` and `IGGY_ROOT_PASSWORD`.** Without them the server
generates a random root password and only logs it once, which a test can't
easily pick up.
+- **`IGGY_TCP_ADDRESS=0.0.0.0:8090` and
`IGGY_NODE_ADVERTISED_ADDRESS=localhost`.** By default the server listens on
`127.0.0.1` inside the container, which a mapped port can't reach. Listening on
all addresses also needs an advertised address, or the server won't start.
+- **`IGGY_SHARDING_CPU_ALLOCATION=all`** on Docker Desktop, WSL2 and other
hosts with a single NUMA node. Without it, 0.9.0 exits with `failed to bind
shard 0 memory to its NUMA node`. It does no harm elsewhere.
+
+Wait for the `server client listeners started` log line before connecting. The
server logs `server running; waiting on shard threads` earlier, before the port
is open, and connecting on that line fails some of the time.
+
+If the server exits with `io_uring was denied locked memory`, the container's
locked-memory limit is too low for it. Raise it, as the `--ulimit
memlock=-1:-1` flag does in [Docker & Helm](/docs/server/docker).
+
+## Rust
+
+This example uses `testcontainers` 0.27 and the `iggy` crate 0.11, which goes
with server 0.9.0.
+
+```toml
+[dev-dependencies]
+iggy = "0.11"
+tokio = { version = "1", features = ["full"] }
+testcontainers = "0.27"
+```
+
+```rust
+use iggy::prelude::*;
+use std::str::FromStr;
+use testcontainers::core::{IntoContainerPort, WaitFor};
+use testcontainers::runners::AsyncRunner;
+use testcontainers::{GenericImage, ImageExt};
+
+#[tokio::test]
+async fn send_and_poll_roundtrip() {
+ let container = GenericImage::new("apache/iggy", "0.9.0")
+ .with_exposed_port(8090.tcp())
+ .with_wait_for(WaitFor::message_on_stdout(
+ "server client listeners started",
+ ))
+ .with_env_var("IGGY_ROOT_USERNAME", "iggy")
+ .with_env_var("IGGY_ROOT_PASSWORD", "iggy")
+ .with_env_var("IGGY_TCP_ADDRESS", "0.0.0.0:8090")
+ .with_env_var("IGGY_NODE_ADVERTISED_ADDRESS", "localhost")
+ .with_env_var("IGGY_SHARDING_CPU_ALLOCATION", "all")
+ .with_security_opt("seccomp=unconfined")
+ .start()
+ .await
+ .expect("failed to start apache/iggy container");
+
+ let host = container.get_host().await.unwrap();
+ let port = container.get_host_port_ipv4(8090).await.unwrap();
+
+ let client = IggyClientBuilder::new()
+ .with_tcp()
+ .with_server_address(format!("{host}:{port}"))
+ .build()
+ .unwrap();
+ client.connect().await.unwrap();
+ client
+ .login_user(DEFAULT_ROOT_USERNAME, DEFAULT_ROOT_PASSWORD)
+ .await
+ .unwrap();
+
+ let stream = client.create_stream("test-stream").await.unwrap();
+ let topic = client
+ .create_topic(
+ &Identifier::named("test-stream").unwrap(),
+ "test-topic",
+ &TopicCreateOptions {
+ partitions_count: Some(1),
+ message_expiry: Some(IggyExpiry::NeverExpire),
+ ..TopicCreateOptions::default()
+ },
+ )
+ .await
+ .unwrap();
+
+ let mut messages = vec![IggyMessage::from_str("hello from cargo
test").unwrap()];
+ client
+ .send_messages(
+ &Identifier::numeric(stream.id).unwrap(),
+ &Identifier::numeric(topic.id).unwrap(),
+ &Partitioning::partition_id(0),
+ &mut messages,
+ )
+ .await
+ .unwrap();
+
+ let consumer = Consumer::default();
+ let polled = client
+ .poll_messages(
+ &Identifier::numeric(stream.id).unwrap(),
+ &Identifier::numeric(topic.id).unwrap(),
+ Some(0),
+ &consumer,
+ &PollingStrategy::offset(0),
+ 1,
+ true,
+ )
+ .await
+ .unwrap();
+
+ assert_eq!(polled.messages.len(), 1);
+ assert_eq!(polled.messages[0].payload.as_ref(), b"hello from cargo test");
+}
+```
+
+Run it with `cargo test`.
+
+## Python
+
+This example uses `testcontainers`, `pytest` and `pytest-asyncio` with
`apache-iggy` 0.9.0:
+
+```bash
+pip install apache-iggy==0.9.0 testcontainers pytest pytest-asyncio
+```
+
+```python
+import asyncio
+from datetime import timedelta
+
+import pytest
+from testcontainers.core.container import DockerContainer
+from testcontainers.core.wait_strategies import LogMessageWaitStrategy
+
+from apache_iggy import (
+ AutoLogin,
+ Consumer,
+ IggyClient,
+ PollingStrategy,
+ SendMessage,
+ TcpConfig,
+ TcpReconnectionConfig,
+)
+
+
[email protected](scope="module")
+def iggy_container():
+ container = (
+ DockerContainer("apache/iggy:0.9.0")
+ .with_exposed_ports(8090)
+ .with_env("IGGY_ROOT_USERNAME", "iggy")
+ .with_env("IGGY_ROOT_PASSWORD", "iggy")
+ .with_env("IGGY_TCP_ADDRESS", "0.0.0.0:8090")
+ .with_env("IGGY_NODE_ADVERTISED_ADDRESS", "localhost")
+ .with_env("IGGY_SHARDING_CPU_ALLOCATION", "all")
+ .with_kwargs(security_opt=["seccomp=unconfined"])
+ .waiting_for(LogMessageWaitStrategy("server client listeners started"))
+ )
+ with container:
+ yield container
+
+
[email protected]
+async def test_send_and_poll_roundtrip(iggy_container):
+ host = iggy_container.get_container_host_ip()
+ port = iggy_container.get_exposed_port(8090)
+
+ client = IggyClient(
+ TcpConfig(
+ server_address=f"{host}:{port}",
+ auto_login=AutoLogin.username_password("iggy", "iggy"),
+ reconnection=TcpReconnectionConfig(
+ enabled=True, interval=timedelta(seconds=1)
+ ),
+ )
+ )
+ await client.connect()
+
+ await client.create_stream(name="test-stream")
+ await client.create_topic(
+ stream="test-stream", partitions_count=1, name="test-topic"
+ )
+
+ await client.send_messages(
+ stream="test-stream",
+ topic="test-topic",
+ partitioning=0,
+ messages=[SendMessage("hello from pytest")],
+ )
+
+ for _ in range(10):
+ polled = await client.poll_messages(
+ stream="test-stream",
+ topic="test-topic",
+ consumer=Consumer.Single("test-consumer"),
+ partition_id=0,
+ polling_strategy=PollingStrategy.Next(),
+ count=1,
+ auto_commit=True,
+ )
+ if polled:
+ break
+ await asyncio.sleep(0.5)
+
+ assert len(polled) == 1
+ assert polled[0].payload().decode("utf-8") == "hello from pytest"
+```
+
+Run it with `pytest -v`.
+
+## Node.js
+
+This example uses `testcontainers` with `apache-iggy` 0.10.0, the Node.js SDK
that goes with server 0.9.0, and the built-in `node:test` runner.
`testcontainers` needs Node.js 22.19 or newer.
+
+```bash
+npm install apache-iggy testcontainers
+```
+
+```javascript
+import assert from 'node:assert/strict';
+import { test, before, after } from 'node:test';
+import { GenericContainer, Wait } from 'testcontainers';
+import { Client, Partitioning, Consumer, PollingStrategy } from 'apache-iggy';
+
+let container;
+let client;
+
+before(async () => {
+ container = await new GenericContainer('apache/iggy:0.9.0')
+ .withExposedPorts(8090)
+ .withEnvironment({
+ IGGY_ROOT_USERNAME: 'iggy',
+ IGGY_ROOT_PASSWORD: 'iggy',
+ IGGY_TCP_ADDRESS: '0.0.0.0:8090',
+ IGGY_NODE_ADVERTISED_ADDRESS: 'localhost',
+ IGGY_SHARDING_CPU_ALLOCATION: 'all',
+ })
+ .withSecurityOpt('seccomp=unconfined')
+ .withWaitStrategy(Wait.forLogMessage('server client listeners started'))
+ .start();
+
+ client = new Client({
+ transport: 'TCP',
+ options: {
+ host: container.getHost(),
+ port: container.getMappedPort(8090),
+ keepAlive: true,
+ },
+ reconnect: { enabled: true, interval: 5000, maxRetries: 5 },
+ heartbeatInterval: 5000,
+ credentials: { username: 'iggy', password: 'iggy' },
+ });
+});
+
+after(async () => {
+ await client?.destroy();
+ await container?.stop();
+});
+
+test('send and poll roundtrip', async () => {
+ const stream = await client.stream.create({ name: 'test-stream' });
+ const topic = await client.topic.create({
+ streamId: stream.id,
+ name: 'test-topic',
+ partitionCount: 1,
+ compressionAlgorithm: 1,
+ });
+
+ await client.message.send({
+ streamId: stream.id,
+ topicId: topic.id,
+ messages: [{ id: 1, headers: [], payload: 'hello from node:test' }],
+ partition: Partitioning.PartitionId(topic.partitions[0].id),
+ });
+
+ let polled;
+ for (let i = 0; i < 10; i++) {
+ polled = await client.message.poll({
+ streamId: stream.id,
+ topicId: topic.id,
+ consumer: Consumer.Single,
+ partitionId: topic.partitions[0].id,
+ pollingStrategy: PollingStrategy.Offset(0n),
+ count: 1,
+ autocommit: false,
+ });
+ if (polled?.messages?.length) break;
+ await new Promise((resolve) => setTimeout(resolve, 500));
+ }
+
+ assert.equal(polled.messages.length, 1);
+ assert.equal(polled.messages[0].payload.toString('utf-8'), 'hello from
node:test');
+});
+```
+
+Save it as `iggy.test.mjs` and run it with `node --test`.