Events
Publish and subscribe to fire-and-forget CloudEvents over HTTP with fan-out delivery and load-balancing consumer groups.
Events are fire-and-forget pub/sub messages over the CloudEvents connector. A publisher sends a CloudEvent with POST /ce/send/event and returns immediately; every subscriber connected to the channel receives every event over an SSE stream.
Overview
The Events pattern maps the KubeMQ Events messaging primitive onto plain HTTP. Publishers do not wait for delivery confirmation, and events are not persisted — a subscriber must be connected at the moment of delivery to receive a message. Connect every subscriber to a channel and you get fan-out (each one receives every event); add a shared group and you get a consumer group where the server load-balances each event to exactly one member.
Use this pattern for real-time notifications, telemetry, and broadcast scenarios where missed messages are acceptable. When you need durability and replay, use Events Store instead.
| Operation | Endpoint | Method |
|---|---|---|
| Publish | /ce/send/event | POST |
| Subscribe | /ce/subscribe/events | GET (SSE) |
How it works
A publisher posts one CloudEvent to the connector, which fans it out to every connected subscriber on the channel; subscribers in a shared group split the load round-robin.
A published CloudEvent fans out to all subscribers; a shared group load-balances it to one member.
Publishing
Send a CloudEvent in structured mode (application/cloudevents+json). The subject attribute selects the KubeMQ channel; the connector replies with HTTP 202 Accepted and a {"is_error": false, ...} body. Each native example below opens an SSE subscriber, publishes one event, prints it, and exits.
curl -X POST http://localhost:9090/ce/send/event \
-H "Content-Type: application/cloudevents+json" \
-d '{
"specversion": "1.0",
"type": "com.example.order.created",
"source": "order-service",
"subject": "orders",
"datacontenttype": "application/json",
"data": {"order_id": "12345", "amount": 99.99}
}'// Example: events/BasicPubSub
// Run: dotnet run
using CloudNative.CloudEvents;
using CloudNative.CloudEvents.SystemTextJson;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
static string ServerUrl() =>
Environment.GetEnvironmentVariable("KUBEMQ_CE_URL") ?? "http://localhost:9090";
var base_ = ServerUrl();
var channel = "csharp-ce-events.basic-pubsub";
var clientId = "kubemq-ce-csharp-example";
var received = new TaskCompletionSource<JsonElement>(
TaskCreationOptions.RunContinuationsAsynchronously);
// Start SSE subscriber.
var subscriberTask = Task.Run(async () =>
{
using var httpClient = new HttpClient { Timeout = Timeout.InfiniteTimeSpan };
var sseUrl = $"{base_}/ce/subscribe/events?client_id={clientId}-sub&channel={Uri.EscapeDataString(channel)}";
using var request = new HttpRequestMessage(HttpMethod.Get, sseUrl);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream"));
request.Headers.CacheControl = new CacheControlHeaderValue { NoCache = true };
using var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
using var stream = await response.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream, Encoding.UTF8);
string? eventType = null, data = null;
string? line;
while ((line = await reader.ReadLineAsync()) != null)
{
if (line == "")
{
if (eventType == "cloudevent" && data != null)
{
received.TrySetResult(JsonSerializer.Deserialize<JsonElement>(data));
return;
}
eventType = null; data = null;
}
else if (line.StartsWith(":")) { /* keepalive */ }
else if (line.StartsWith("event:")) eventType = line["event:".Length..].Trim();
else if (line.StartsWith("data:")) data = line["data:".Length..].Trim();
}
});
// Allow subscription to establish.
await Task.Delay(500);
// Build and publish CloudEvent (structured mode).
var formatter = new JsonEventFormatter();
var cloudEvent = new CloudEvent
{
Id = Guid.NewGuid().ToString(),
Type = "com.kubemq.examples.events.sent",
Source = new Uri($"urn:{clientId}"),
Subject = channel,
DataContentType = "application/json",
Data = new { message = "Hello from C# CloudEvents example!" },
};
cloudEvent.SetAttributeFromString("time", DateTimeOffset.UtcNow.ToString("O"));
var eventBytes = formatter.EncodeStructuredModeMessage(cloudEvent, out var contentType);
using var httpClient = new HttpClient();
using var content = new ByteArrayContent(eventBytes.ToArray());
content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType.ToString());
var resp = await httpClient.PostAsync($"{base_}/ce/send/event", content);
var resultJson = await resp.Content.ReadAsStringAsync();
using var resultDoc = JsonDocument.Parse(resultJson);
Console.WriteLine($"Published: status={resp.StatusCode} is_error={resultDoc.RootElement.GetProperty("is_error")}");
// Wait for the event.
var ce = await received.Task;
Console.WriteLine($"Received: {ce.GetProperty("type")} / {ce.GetProperty("data")}");// Example: events/basic-pubsub
// Run: go run ./events/basic-pubsub/main.go
package main
import (
"bufio"
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"strings"
"time"
cloudevents "github.com/cloudevents/sdk-go/v2"
)
func serverURL() string {
if u := os.Getenv("KUBEMQ_CE_URL"); u != "" {
return u
}
return "http://localhost:9090"
}
func main() {
base := serverURL()
channel := "go-ce-events.basic-pubsub"
clientID := "kubemq-ce-go-example"
received := make(chan string, 1)
// Start SSE subscriber in background goroutine.
go func() {
sseURL := fmt.Sprintf("%s/ce/subscribe/events?client_id=%s&channel=%s",
base, clientID+"-sub", channel)
req, _ := http.NewRequest("GET", sseURL, nil)
req.Header.Set("Accept", "text/event-stream")
req.Header.Set("Cache-Control", "no-cache")
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal("SSE connect:", err)
}
defer resp.Body.Close()
scanner := bufio.NewScanner(resp.Body)
var eventType, data string
for scanner.Scan() {
line := scanner.Text()
if line == "" {
if eventType == "cloudevent" && data != "" {
received <- data
return
}
eventType, data = "", ""
continue
}
if strings.HasPrefix(line, ":") {
continue // keepalive
}
if strings.HasPrefix(line, "event:") {
eventType = strings.TrimSpace(strings.TrimPrefix(line, "event:"))
} else if strings.HasPrefix(line, "data:") {
data = strings.TrimSpace(strings.TrimPrefix(line, "data:"))
}
}
}()
// Allow SSE subscription to establish.
time.Sleep(500 * time.Millisecond)
// Build and send CloudEvent (structured mode).
event := cloudevents.NewEvent()
event.SetType("com.kubemq.examples.events.sent")
event.SetSource("kubemq-ce-go-example")
event.SetSubject(channel) // subject = KubeMQ channel
_ = event.SetData(cloudevents.ApplicationJSON, map[string]string{
"message": "Hello from Go CloudEvents example!",
})
body, _ := json.Marshal(event)
req, _ := http.NewRequest("POST", base+"/ce/send/event", strings.NewReader(string(body)))
req.Header.Set("Content-Type", "application/cloudevents+json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal("send event:", err)
}
defer resp.Body.Close()
var result map[string]interface{}
_ = json.NewDecoder(resp.Body).Decode(&result)
fmt.Printf("Published: status=%d is_error=%v\n", resp.StatusCode, result["is_error"])
// Wait for subscriber to receive the event.
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
select {
case data := <-received:
var ce map[string]interface{}
_ = json.Unmarshal([]byte(data), &ce)
fmt.Printf("Received: type=%v subject=%v data=%v\n", ce["type"], ce["subject"], ce["data"])
case <-ctx.Done():
log.Fatal("Timed out waiting for event")
}
}// Example: events/basic-pubsub
// Run: mvn compile exec:java
package io.kubemq.examples.events.basicpubsub;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.cloudevents.CloudEvent;
import io.cloudevents.core.builder.CloudEventBuilder;
import io.cloudevents.core.format.EventFormat;
import io.cloudevents.core.provider.EventFormatProvider;
import io.cloudevents.jackson.JsonFormat;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.OffsetDateTime;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
public class Main {
static String serverUrl() {
String u = System.getenv("KUBEMQ_CE_URL");
return (u != null && !u.isEmpty()) ? u : "http://localhost:9090";
}
static final ObjectMapper MAPPER = new ObjectMapper();
public static void main(String[] args) throws Exception {
String base = serverUrl();
String channel = "java-ce-events.basic-pubsub";
String clientId = "kubemq-ce-java-example";
BlockingQueue<String> received = new ArrayBlockingQueue<>(1);
// Start SSE subscriber in background thread.
String sseUrl = base + "/ce/subscribe/events?client_id=" + clientId
+ "-sub&channel=" + channel;
Thread subscriber = Thread.ofVirtual().start(() -> {
try {
HttpURLConnection conn = (HttpURLConnection) new URL(sseUrl).openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "text/event-stream");
conn.setRequestProperty("Cache-Control", "no-cache");
conn.setDoInput(true);
conn.setReadTimeout(15_000);
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
String line;
String eventType = null, data = null;
while ((line = reader.readLine()) != null) {
if (line.isEmpty()) {
if ("cloudevent".equals(eventType) && data != null) {
received.offer(data);
return;
}
eventType = null;
data = null;
} else if (line.startsWith(":")) {
// keepalive
} else if (line.startsWith("event:")) {
eventType = line.substring("event:".length()).trim();
} else if (line.startsWith("data:")) {
data = line.substring("data:".length()).trim();
}
}
}
} catch (Exception e) {
System.err.println("SSE error: " + e.getMessage());
}
});
// Allow subscription to establish.
Thread.sleep(500);
// Build CloudEvent (structured mode).
EventFormatProvider.getInstance().registerFormat(new JsonFormat());
EventFormat format = EventFormatProvider.getInstance().resolveFormat(JsonFormat.CONTENT_TYPE);
CloudEvent event = CloudEventBuilder.v1()
.withId(UUID.randomUUID().toString())
.withType("com.kubemq.examples.events.sent")
.withSource(URI.create(clientId))
.withSubject(channel)
.withDataContentType("application/json")
.withTime(OffsetDateTime.now())
.withData("application/json",
MAPPER.writeValueAsBytes(Map.of("message", "Hello from Java CloudEvents example!")))
.build();
byte[] body = format.serialize(event);
HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(base + "/ce/send/event"))
.POST(HttpRequest.BodyPublishers.ofByteArray(body))
.header("Content-Type", "application/cloudevents+json")
.build();
HttpResponse<String> response = httpClient.send(request,
HttpResponse.BodyHandlers.ofString());
Map<?, ?> result = MAPPER.readValue(response.body(), Map.class);
System.out.printf("Published: status=%d is_error=%s%n",
response.statusCode(), result.get("is_error"));
// Wait for event.
String data = received.poll(10, TimeUnit.SECONDS);
Map<?, ?> ce = MAPPER.readValue(data, Map.class);
System.out.println("Received: " + ce.get("type") + " / " + ce.get("data"));
subscriber.interrupt();
}
}// Example: events/basic-pubsub
// Run: npx tsx events/basic-pubsub/index.ts
import { CloudEvent, HTTP } from 'cloudevents';
import EventSource from 'eventsource';
function serverUrl(): string {
return process.env.KUBEMQ_CE_URL ?? 'http://localhost:9090';
}
async function waitForEvent(
base: string,
channel: string,
clientId: string,
): Promise<Record<string, unknown>> {
return new Promise((resolve, reject) => {
const sseUrl = `${base}/ce/subscribe/events?client_id=${clientId}&channel=${encodeURIComponent(channel)}`;
const es = new EventSource(sseUrl);
const timer = setTimeout(() => {
es.close();
reject(new Error('Timed out waiting for event'));
}, 10_000);
es.addEventListener('cloudevent', (evt: MessageEvent) => {
clearTimeout(timer);
es.close();
resolve(JSON.parse(evt.data) as Record<string, unknown>);
});
});
}
async function main(): Promise<void> {
const base = serverUrl();
const channel = 'js-ce-events.basic-pubsub';
const clientId = 'kubemq-ce-js-example';
// Start waiting for event (opens SSE stream).
const eventPromise = waitForEvent(base, channel, `${clientId}-sub`);
// Allow SSE connection to establish.
await new Promise((r) => setTimeout(r, 500));
// Build and publish CloudEvent (structured mode).
const event = new CloudEvent({
type: 'com.kubemq.examples.events.sent',
source: clientId,
subject: channel,
datacontenttype: 'application/json',
data: { message: 'Hello from JavaScript/TypeScript CloudEvents example!' },
});
const message = HTTP.structured(event);
const resp = await fetch(`${base}/ce/send/event`, {
method: 'POST',
headers: message.headers as Record<string, string>,
body: message.body as string,
});
const result = await resp.json() as { is_error: boolean };
console.log(`Published: status=${resp.status} is_error=${result.is_error}`);
// Wait for subscriber.
const received = await eventPromise;
console.log(`Received: ${received.type} / ${JSON.stringify(received.data)}`);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});# Example: events/basic_pubsub
# Run: python events/basic_pubsub/main.py
from __future__ import annotations
import json
import os
import threading
import time
import requests
from cloudevents.v1.conversion import to_structured
from cloudevents.v1.http import CloudEvent
def server_url() -> str:
return os.environ.get("KUBEMQ_CE_URL", "http://localhost:9090")
def subscribe(base: str, channel: str, client_id: str, received: list[str]) -> None:
"""Open SSE stream and collect one cloudevent."""
sse_url = (
f"{base}/ce/subscribe/events"
f"?client_id={client_id}&channel={channel}"
)
with requests.get(sse_url, stream=True, timeout=None,
headers={"Accept": "text/event-stream",
"Cache-Control": "no-cache"}) as resp:
event_type = ""
data = ""
for line in resp.iter_lines(decode_unicode=True):
if line == "":
if event_type == "cloudevent" and data:
received.append(data)
return
event_type = ""
data = ""
continue
if line.startswith(":"):
continue # keepalive
if line.startswith("event:"):
event_type = line[len("event:"):].strip()
elif line.startswith("data:"):
data = line[len("data:"):].strip()
def main() -> None:
base = server_url()
channel = "python-ce-events.basic-pubsub"
client_id = "kubemq-ce-python-example"
received: list[str] = []
# Start subscriber in background thread.
t = threading.Thread(
target=subscribe,
args=(base, channel, client_id + "-sub", received),
daemon=True,
)
t.start()
# Allow SSE connection to establish.
time.sleep(0.5)
# Build and send CloudEvent (structured mode).
event = CloudEvent(
attributes={
"type": "com.kubemq.examples.events.sent",
"source": client_id,
"subject": channel,
"datacontenttype": "application/json",
},
data={"message": "Hello from Python CloudEvents example!"},
)
headers, body = to_structured(event)
resp = requests.post(
f"{base}/ce/send/event",
data=body,
headers=dict(headers),
timeout=10,
)
result = resp.json()
print(f"Published: status={resp.status_code} is_error={result.get('is_error')}")
# Wait for subscriber.
deadline = time.time() + 10
while not received and time.time() < deadline:
time.sleep(0.1)
ce = json.loads(received[0])
print(f"Received: {ce.get('type')} / {ce.get('data')}")
if __name__ == "__main__":
main()# Example: events/basic_pubsub
# Run: ruby events/basic_pubsub/main.rb
require "net/http"
require "uri"
require "json"
require "timeout"
require "securerandom"
require "cloud_events"
def server_url
ENV.fetch("KUBEMQ_CE_URL", "http://localhost:9090")
end
base = server_url
channel = "ruby-ce-events.basic-pubsub"
client_id = "kubemq-ce-ruby-example"
received = Queue.new
# SSE subscriber thread.
subscriber = Thread.new do
uri = URI("#{base}/ce/subscribe/events?client_id=#{client_id}-sub&channel=#{URI.encode_www_form_component(channel)}")
Net::HTTP.start(uri.host, uri.port) do |http|
req = Net::HTTP::Get.new(uri)
req["Accept"] = "text/event-stream"
req["Cache-Control"] = "no-cache"
http.request(req) do |resp|
ev_type = nil
data = nil
resp.read_body do |chunk|
chunk.each_line do |line|
line.chomp!
if line.empty?
if ev_type == "cloudevent" && data
received.push(data)
Thread.exit
end
ev_type = nil
data = nil
elsif line.start_with?(":") # keepalive
elsif line.start_with?("event:")
ev_type = line.sub("event:", "").strip
elsif line.start_with?("data:")
data = line.sub("data:", "").strip
end
end
end
end
end
end
# Allow subscription to establish.
sleep 0.5
# Build and publish CloudEvent (structured mode).
sdk = CloudEvents::HttpBinding.default
event = CloudEvents::Event::V1.new(
id: SecureRandom.uuid,
type: "com.kubemq.examples.events.sent",
source: URI("urn:#{client_id}"),
subject: channel,
spec_version: "1.0",
data_content_type: CloudEvents::ContentType.new("application/json"),
data: JSON.generate({ message: "Hello from Ruby CloudEvents example!" })
)
# Encode as structured mode.
headers, body = sdk.encode_event(event, structured_format: "json")
uri = URI("#{base}/ce/send/event")
Net::HTTP.start(uri.host, uri.port) do |http|
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = headers["Content-Type"]
req.body = body
res = http.request(req)
result = JSON.parse(res.body)
puts "Published: status=#{res.code} is_error=#{result['is_error']}"
end
# Wait for subscriber.
data = nil
Timeout.timeout(10) { data = received.pop }
ce = JSON.parse(data)
puts "Received: #{ce['type']} / #{ce['data']}"//! Example: events/basic-pubsub
//! Run: cargo run -p basic-pubsub
use bytes::Bytes;
use cloudevents::{EventBuilder, EventBuilderV10};
use futures_util::StreamExt;
use reqwest::Client;
use serde_json::{json, Value};
use std::env;
use tokio::sync::oneshot;
use uuid::Uuid;
fn server_url() -> String {
env::var("KUBEMQ_CE_URL").unwrap_or_else(|_| "http://localhost:9090".to_string())
}
/// Parse SSE lines and return the data of the first cloudevent.
async fn wait_for_cloudevent(
mut stream: impl futures_util::Stream<Item = reqwest::Result<Bytes>> + Unpin,
tx: oneshot::Sender<String>,
) {
let mut event_type = String::new();
let mut data = String::new();
let mut buffer = String::new();
while let Some(chunk) = stream.next().await {
let chunk = match chunk {
Ok(c) => c,
Err(e) => { eprintln!("SSE read error: {}", e); break; }
};
buffer.push_str(&String::from_utf8_lossy(&chunk));
while let Some(pos) = buffer.find('\n') {
let line = buffer[..pos].trim_end_matches('\r').to_string();
buffer = buffer[pos + 1..].to_string();
if line.is_empty() {
if event_type == "cloudevent" && !data.is_empty() {
let _ = tx.send(data.clone());
return;
}
event_type.clear();
data.clear();
} else if line.starts_with(':') {
// keepalive comment — ignore
} else if let Some(v) = line.strip_prefix("event:") {
event_type = v.trim().to_string();
} else if let Some(v) = line.strip_prefix("data:") {
data = v.trim().to_string();
}
}
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let base = server_url();
let channel = "rust-ce-events.basic-pubsub";
let client_id = "kubemq-ce-rust-example";
let client = Client::new();
// Start SSE subscriber.
let (tx, rx) = oneshot::channel::<String>();
let sub_url = format!(
"{}/ce/subscribe/events?client_id={}-sub&channel={}",
base, client_id, channel
);
let sub_client = client.clone();
tokio::spawn(async move {
let stream = sub_client
.get(&sub_url)
.header("Accept", "text/event-stream")
.header("Cache-Control", "no-cache")
.send()
.await
.expect("SSE connect failed")
.bytes_stream();
wait_for_cloudevent(stream, tx).await;
});
// Allow SSE to establish.
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
// Build CloudEvent (structured mode using cloudevents-sdk).
let event = EventBuilderV10::new()
.id(Uuid::new_v4().to_string())
.ty("com.kubemq.examples.events.sent")
.source(format!("urn:{}", client_id))
.subject(channel)
.data(
"application/json",
json!({"message": "Hello from Rust CloudEvents example!"}),
)
.build()?;
// Serialize to structured mode JSON.
let body = serde_json::to_string(&event)?;
let resp = client
.post(format!("{}/ce/send/event", base))
.header("Content-Type", "application/cloudevents+json")
.body(body)
.send()
.await?;
let result: Value = resp.json().await?;
println!("Published: status=202 is_error={}", result["is_error"]);
// Wait for received event.
let data = tokio::time::timeout(tokio::time::Duration::from_secs(10), rx)
.await
.expect("Timed out waiting for event")
.expect("Channel closed");
let ce: Value = serde_json::from_str(&data)?;
println!("Received: {} / {}", ce["type"], ce["data"]);
Ok(())
}The publish call returns HTTP 202 Accepted:
{ "is_error": false, "message": "OK", "data": {} }Subscribing
Open a long-lived SSE stream with GET /ce/subscribe/events. The connector streams event: cloudevent frames as messages arrive; each data: line holds the reconstructed CloudEvent JSON.
curl -N "http://localhost:9090/ce/subscribe/events?client_id=my-client&channel=orders"A delivered frame looks like:
event: cloudevent
data: {"specversion":"1.0","type":"com.example.order.created","source":"order-service","subject":"orders","data":{"order_id":"12345","amount":99.99}}
Subscription accepts these query parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
client_id | string | Yes | Client identifier (overridden by auth claims when auth is enabled) |
channel | string | Yes | Channel to subscribe to |
group | string | No | Load-balancing group name |
Consumer groups
Without a group, every subscriber on a channel receives every event (fan-out). Add the same group to multiple subscribers and they form a load-balancing pool — each event is delivered to exactly one member, round-robin.
# Terminal 1 — worker 1 in group "workers"
curl -N "http://localhost:9090/ce/subscribe/events?client_id=w1&channel=orders&group=workers"
# Terminal 2 — worker 2 in the same group
curl -N "http://localhost:9090/ce/subscribe/events?client_id=w2&channel=orders&group=workers"// Example: events/consumer-group — each subscriber gets one of two events.
func subscribe(base, clientID, channel, group string, results chan<- string, wg *sync.WaitGroup) {
defer wg.Done()
sseURL := fmt.Sprintf("%s/ce/subscribe/events?client_id=%s&channel=%s&group=%s",
base, clientID, channel, group)
req, _ := http.NewRequest("GET", sseURL, nil)
req.Header.Set("Accept", "text/event-stream")
req.Header.Set("Cache-Control", "no-cache")
client := &http.Client{Timeout: 0} // no timeout for SSE
resp, err := client.Do(req)
if err != nil {
log.Printf("[%s] SSE connect error: %v", clientID, err)
return
}
defer resp.Body.Close()
scanner := bufio.NewScanner(resp.Body)
var eventType, data string
for scanner.Scan() {
line := scanner.Text()
if line == "" {
if eventType == "cloudevent" && data != "" {
results <- fmt.Sprintf("[%s] received: %s", clientID, data)
return
}
eventType, data = "", ""
continue
}
if strings.HasPrefix(line, ":") {
continue
}
if strings.HasPrefix(line, "event:") {
eventType = strings.TrimSpace(strings.TrimPrefix(line, "event:"))
} else if strings.HasPrefix(line, "data:") {
data = strings.TrimSpace(strings.TrimPrefix(line, "data:"))
}
}
}
func main() {
base := serverURL()
channel := "go-ce-events.consumer-group"
group := "workers"
results := make(chan string, 2)
var wg sync.WaitGroup
// Two subscribers in the same group.
for i := 1; i <= 2; i++ {
wg.Add(1)
go subscribe(base, fmt.Sprintf("worker-%d", i), channel, group, results, &wg)
}
time.Sleep(600 * time.Millisecond)
// Publish two events — each subscriber should get exactly one.
sendEvent(base, channel, 1)
sendEvent(base, channel, 2)
deadline := time.After(10 * time.Second)
for received := 0; received < 2; {
select {
case msg := <-results:
fmt.Println(msg)
received++
case <-deadline:
log.Fatal("Timed out waiting for events")
}
}
}# Example: events/consumer_group — load-balanced subscriber groups.
import threading
import time
import requests
from cloudevents.v1.conversion import to_structured
from cloudevents.v1.http import CloudEvent
def subscribe_group(base, channel, group, client_id, results, stop):
sse_url = (f"{base}/ce/subscribe/events"
f"?client_id={client_id}&channel={channel}&group={group}")
with requests.get(sse_url, stream=True, timeout=None,
headers={"Accept": "text/event-stream"}) as resp:
ev_type = ""
data = ""
for line in resp.iter_lines(decode_unicode=True):
if stop.is_set():
return
if line == "":
if ev_type == "cloudevent" and data:
results.append(f"[{client_id}] received: {data[:80]}")
return
ev_type = data = ""
continue
if line.startswith(":"):
continue
if line.startswith("event:"):
ev_type = line[6:].strip()
elif line.startswith("data:"):
data = line[5:].strip()
def main():
base = "http://localhost:9090"
channel = "python-ce-events.consumer-group"
group = "workers"
results, stop = [], threading.Event()
for i in range(1, 3):
threading.Thread(
target=subscribe_group,
args=(base, channel, group, f"worker-{i}", results, stop),
daemon=True,
).start()
time.sleep(0.6)
for seq in range(1, 3):
event = CloudEvent(
attributes={
"type": "com.kubemq.examples.events.grouped",
"source": "kubemq-ce-python-example",
"subject": channel,
"datacontenttype": "application/json",
},
data={"seq": seq},
)
headers, body = to_structured(event)
resp = requests.post(f"{base}/ce/send/event", data=body,
headers=dict(headers), timeout=10)
print(f"Published event seq={seq} (status={resp.status_code})")
deadline = time.time() + 10
while len(results) < 2 and time.time() < deadline:
time.sleep(0.1)
stop.set()
for r in results:
print(r)
if __name__ == "__main__":
main()Consumer groups are server-side and ephemeral — they exist only while subscribers are connected. Because events are not persisted, a group that has no connected members when an event is published will not receive it. For durable, replayable delivery, use Events Store.
Related
Events Store
Persistent events with replay via Last-Event-ID and six start positions.
Content modes
Structured vs binary CloudEvents and how the connector detects each.
SSE behavior
Wire format, keepalives, idle timeouts, and reconnection.
Endpoints reference
Full endpoint table, status codes, and the response envelope.
Was this page helpful?