Skip to content

Messaging API

This is the API reference for the messaging subsystem: every public type and method signature, per language. For the conceptual walkthrough (transports, the wire envelope, concurrency caps, the broker config rules), read the Messaging guide first — this page is the exhaustive surface.

All signatures are verified against source. Java is canonical; Python, Rust, and TypeScript mirror it. The same publish/subscribe, request/reply, and message envelope work identically across all four languages, so components written in any language interoperate on the same topics.

Topics are minted through the UNS builder (gg.uns() / gg.instance(id).uns()); the messaging client itself takes literal topics and enforces one rule — the reserved-class publish guard: a client-chosen publish/request/reply to ecv1/.../{state|metric|cfg|log} is rejected (ReservedTopicException / ReservedTopicError / EdgeCommonsError::ReservedTopic).

How a component reaches the messaging service from its EdgeCommons handle.

Returns a MessagingClient instance (instance methods).

MessagingClient getMessaging(); // EdgeCommons.getMessaging()

The primary type carrying all publish/subscribe/request/reply methods. Plain methods target the local broker (or Greengrass IPC); northbound methods target the configured northbound transport and take a QoS. The northbound transport is a generic MQTT broker in standalone mode and the Greengrass IoT Core bridge in Greengrass mode.

MessagingClient — instance methods. Built internally via MessagingClientBuilder.create(parsedCommandLine).withReceiveOwnMessages(boolean).build() (receiveOwnMessages defaults to true); you normally obtain it from gg.getMessaging(). receiveOwnMessages is consumed only by the Greengrass IPC provider (it selects the IPC ReceiveModeRECEIVE_ALL_MESSAGES vs RECEIVE_MESSAGES_FROM_OTHERS); the standalone/MQTT provider takes no such flag and ignores it (an MQTT broker has no own-message filter).

public static final int DEFAULT_MAX_MESSAGES = 10_000;
void publish(String topic, Message msg);
void publishNorthbound(String topic, Message msg, Qos qos);
void publishRaw(String topic, JsonObject metricObject);
void publishNorthboundRaw(String topic, JsonObject metricObject, Qos qos);
void subscribe(String topicFilter, BiConsumer<String,Message> callback);
void subscribe(String topicFilter, BiConsumer<String,Message> callback, int maxConcurrency);
void subscribe(String topicFilter, BiConsumer<String,Message> callback, int maxConcurrency, int maxMessages);
void subscribeNorthbound(String topicFilter, BiConsumer<String,Message> callback, Qos qos);
void subscribeNorthbound(String topicFilter, BiConsumer<String,Message> callback, Qos qos, int maxConcurrency);
void subscribeNorthbound(String topicFilter, BiConsumer<String,Message> callback, Qos qos, int maxConcurrency, int maxMessages);
ReplyFuture request(String topic, Message request); // default deadline (30 s)
ReplyFuture request(String topic, Message request, Duration timeout); // null = default, ZERO = off
ReplyFuture requestNorthbound(String topic, Message request);
ReplyFuture requestNorthbound(String topic, Message request, Duration timeout);
void cancelRequest(ReplyFuture replyFuture);
void cancelRequestNorthbound(ReplyFuture replyFuture);
void reply(Message request, Message reply);
void replyNorthbound(Message request, Message reply);
void unsubscribe(String topicFilter);
void unsubscribeNorthbound(String topicFilter);
static boolean topicMatchesFilter(String topicFilter, String topic);
boolean connected();
void close();
Object getNativeLocalClient();
Object getNativeNorthboundClient();
ReservedPublisher reservedPublisher(); // library-internal: the privileged publish seam
// the guard does not apply to (heartbeat/metric/cfg)

The 3-arg subscribe overload’s int is maxConcurrency, NOT maxMessages (the 4-arg overload adds maxMessages last). ReservedPublisher is public-reachable but documented library-internal — component code uses the normal publish methods.

  • maxConcurrency — maximum concurrent handler invocations for the subscription. A value <= 0 is uncapped (Java/Python skip the semaphore; Rust/TS clamp up to 1).
  • maxMessages — bounded in-memory queue depth per subscription; on overflow, new messages are dropped with a warning (the subscription never blocks). Default 10000 in Java/Python (DEFAULT_MAX_MESSAGES), 32 in TypeScript, caller-supplied in Rust.
  • connected() reports the local broker link only (a northbound outage does not flip it); it backs /readyz, never /livez.

The on-wire shape is identical across all four languages: { "header": {...}, "identity": {...}, "tags": {...}, "body": <any> }, or { "raw": <value> } for a raw message. Header keys are snake_case; reply_to is omitted when absent. identity is the publisher’s UNS identity (stamped by a config-bound builder; legally absent on bootstrap/raw-bridged messages); tags carries business context only and is emitted only when tags were stamped. A received payload with none of header/identity/tags/body (or non-JSON bytes) is delivered as a raw message; non-JSON bytes become a raw string. A malformed inbound identity parses leniently to null/absent with a warning — the message still delivers.

Binary bodies are first-class but deliberately bounded: a decoded binary body is limited to 64 KiB and travels inside body as:

{
"_edgecommonsBinary": {
"encoding": "base64",
"length": 5,
"data": "AAEC/v8="
}
}

The normal body accessors return that marker object on inbound messages. Use the binary-specific accessors below to decode and validate it.

Message (package-private fields MessageHeader header; MessageIdentity identity; MessageTags tags; Object body; Object raw;).

JsonObject toDict(); // header, identity, tags, body (identity between header and tags)
String getCorrelationId(); // lazily generates a UUID if null
MessageHeader getHeader();
MessageIdentity getIdentity(); // null when the message carries no identity
MessageTags getTags();
void injectTag(String key, String value);
Object getBody();
boolean isBinaryBody();
byte[] getBinaryBody(); // null when body is not binary; validates marker + 64 KiB limit
Object getRaw();
String makeRequest(); // also: makeRequest(String replyTo)
void setCorrelationId(String id);
// Deprecated static factories — prefer MessageBuilder:
static Message buildFromConfig(...); // @Deprecated
static Message build(Object contents); // @Deprecated

A non-JsonElement payload (Map/POJO/List) passed to the builder is converted via the Gson reflective tree, so withPayload(Map) works.

The header carries message metadata; tags carries business context only (your custom tags config keys). tags has no special thing handling in any language — the publisher’s device travels in the top-level identity element, and a stray inbound thing key just lands in the generic tag map. The reply-topic prefix is the literal string edgecommons/reply- (trailing -, not /) in all four languages; reply topics are deliberately non-UNS.

class MessageHeader {
String name; String version; String timestamp; // ISO-8601 / RFC3339 (ISO_INSTANT)
String correlationId; String uuid; String replyTo;
static final String REPLY_MESSAGE_TOPIC_PREFIX = "edgecommons/reply-";
JsonObject toDict(); // emits name, version, timestamp, uuid, correlation_id, [reply_to]
}
class MessageTags {
JsonObject tags; // the free-form tag map (no thing field)
JsonObject toDict(); // flattens the tag map
static MessageTags fromConfig(ConfigManager config);
void injectTag(String key, String value);
}

For the MessageIdentity type (hier/path/component/instance, the computed device accessor, and withInstance), see the UNS API reference.

Construct messages with the builder, not raw constructors — it stamps uuid, correlation_id, and timestamp for you. Supplying the config is what stamps the custom tags and the UNS identity (with the optional per-message instance token — omit it for a component-scoped message); build() never requires config in any language (a config-less build produces a legal envelope without identity). An explicit identity override always wins over the config-resolved one. All four builders expose explicit uuid/timestamp setters (deterministic envelopes for tests and vectors).

static MessageBuilder create(String name, String version);
static Message fromObject(Object msgContents); // classifies dict→envelope vs raw
MessageBuilder withCorrelationId(String id);
MessageBuilder withUuid(String uuid);
MessageBuilder withTimestamp(String timestamp);
MessageBuilder withPayload(Object payload); // JsonObject / Map / POJO / List
MessageBuilder withConfig(ConfigManager config); // stamps tags + the UNS identity
MessageBuilder withInstance(String instance); // per-message instance token (omit for component scope)
MessageBuilder withIdentity(MessageIdentity id); // explicit override (tests, vectors, relays)
Message build();

A string payload that parses as JSON is stored as the parsed object; otherwise it is stored as a raw string. The companion MessageHeaderBuilder.create(name, version) offers .withCorrelationId/.withTimestamp/.withUuid/.withReplyTo and .build().

Request/reply future and the internal deadline

Section titled “Request/reply future and the internal deadline”

request returns a future that resolves when the matching reply arrives. The responder’s reply(request, reply) copies the request’s correlation_id onto the reply before publishing it to the request’s reply_to topic.

Every request arms a framework-owned deadline at send time — the configured messaging.requestTimeoutSeconds default (30 s; 0 disables) unless a per-call value is given. When it fires, the library (1) unsubscribes the ephemeral reply topic, (2) removes the pending entry, and (3) completes the future exceptionally — even if the caller never awaits it, so an abandoned request cannot leak its reply subscription. Reply-arrival, the deadline, and cancelRequest race through one idempotent settle path; a straggler reply after settle is logged at DEBUG and dropped. As a result, a no-arg get()/await is bounded by the deadline, and a get(t) waits at most min(t, deadline).

Language Per-call override Deadline failure signal
Java request(topic, msg, Duration) (null = default, ZERO = off) java.util.concurrent.TimeoutException (exceptional completion)
Python request(topic, msg, timeout_secs=...) (None = default, 0 = off) Iou.get() raises RequestTimeoutError
Rust request_with_timeout(topic, msg, Option<Duration>) future resolves Err(EdgeCommonsError::RequestTimeout { .. })
TypeScript request(topic, msg, timeoutMs) (omitted = default, 0 = off) promise rejects with RequestTimeoutError

ReplyFuture extends CompletableFuture<Message> — use the standard future API (get(), orTimeout(), thenAccept()). It exposes a public String replyTopic; field.

public class ReplyFuture extends CompletableFuture<Message> {
public String replyTopic;
}
// usage:
ReplyFuture future = messaging.request("svc/op", request); // 30 s default deadline
Message reply = future.get(); // bounded by the deadline
// on deadline: ExecutionException wrapping java.util.concurrent.TimeoutException

MQTT QoS defaults are configured under each broker, messaging.local.qos and messaging.northbound.qos, for operations that do not carry an explicit QoS parameter. The standalone local and northbound MQTT brokers support QoS 0/1/2: 0 is at most once, 1 is at least once, and 2 is exactly once. All four language APIs expose EdgeCommons-owned QoS enums. Greengrass IoT Core IPC accepts only QoS 0/1; calls using QoS 2 fail at the IPC provider boundary. Direct northbound methods that take a QoS argument still use that per-call value.

Destination exists in Rust/TS as a Local / Northbound selector used by the lower-level provider (Rust Destination::Northbound, TypeScript Destination.Northbound — string value "northbound" in both); Java/Python do not expose an equivalent enum because they use per-destination method pairs.

Qos is the Java-native EdgeCommons enum. There is no separate Destination enum — use the plain vs *Northbound method pairs.

import com.mbreissi.edgecommons.messaging.Qos;
Qos.AT_MOST_ONCE; // MQTT QoS 0
Qos.AT_LEAST_ONCE; // MQTT QoS 1
Qos.EXACTLY_ONCE; // MQTT QoS 2; standalone MQTT only

Below the client/service sits a transport MessagingProvider. Rust and TypeScript expose a substitutable seam — the MessagingService trait / IMessagingService interface plus a swappable provider — so you can inject a fake transport in tests. Java and Python have concrete clients only (no service interface, no DI); the two providers are internal.

No service interface or DI. The abstract base MessagingProvider has two concrete subclasses — GreengrassMessagingProvider (IPC) and StandaloneMessagingProvider (dual-MQTT). There is no single-client MqttProvider class. Test against the concrete MessagingClient (point it at a local broker; reset its process-global state between tests).

With the MQTT transport, the broker config is a JSON messaging section with a local block and an optional northbound block, plus the request-deadline default. QoS defaults belong inside each broker block. The wire shape is identical across the four languages (each maps it to its own config type — Java MessagingConfiguration, Python messaging_config.py dataclasses, Rust config.rs, TS config.ts).

{
"messaging": {
"local": {
"host": "localhost",
"port": 8883,
"clientId": "my-component",
"qos": { "publish": 1, "subscribe": 1 },
"credentials": {
"caPath": "/certs/ca.crt",
"certPath": "/certs/client.crt",
"keyPath": "/certs/client.key",
"username": "optional-user",
"password": "optional-pass"
}
},
"northbound": {
"endpoint": "northbound-broker.example.com",
"port": 8883,
"clientId": "my-component",
"qos": { "publish": 1, "subscribe": 1 },
"credentials": {
"caPath": "/certs/northbound-ca.pem",
"certPath": "/certs/device.pem.crt",
"keyPath": "/certs/private.pem.key"
}
},
"requestTimeoutSeconds": 30
}
}

Field reference (verified, identical across all four languages):

  • localhost (string), port (int), clientId (string), optional qos and credentials.
  • northboundhost or endpoint (string), port (int), clientId (string), optional qos and credentials. The whole northbound block is optional: omit it for a local-only (“single-broker”) deployment.
  • credentialscaPath, certPath, keyPath, and optional username / password.
  • requestTimeoutSeconds — number, min 0, default 30; the default request deadline. 0 disables it; an explicit per-call timeout always wins.
  • local.qos / northbound.qos — optional MQTT QoS defaults for operations on that broker without an explicit QoS argument. publish and subscribe accept 0 (at most once), 1 (at least once), or 2 (exactly once).

Rules:

  • Local-broker TLS is keyed solely on caPath presence (useSSL = credentials != null && caPath != null). caPath only ⇒ server-only TLS; caPath + certPath + keyPath ⇒ mutual TLS; a certPath/keyPath without caPath stays plaintext (the cert is ignored). username/password apply only to a plaintext (no-caPath) local broker — they are set in the else branch under if (!useSSL), so they are mutually exclusive with TLS and ignored once caPath is present.
  • A TLS northbound broker may require full mutual-TLS credentials (caPath + certPath + keyPath).
  • host is an opaque string — a Kubernetes Service DNS name works unchanged.
  • local.type is optional and ignored — parsed into the config record but never read; there is no defaulting to "mqtt".
  • The broker file is parsed leniently (plain Gson in Java) and is not schema-validated at runtime; unknown/mistyped keys are silently ignored.

A minimal publish, subscribe, and request/reply round trip in each language.

MessagingClient messaging = gg.getMessaging();
// subscribe — 3rd arg is maxConcurrency
messaging.subscribe("itest/pubsub", (topic, m) -> {
System.out.println(m.getHeader().getName() + " -> " + m.getBody());
}, 1);
// publish
Message msg = MessageBuilder.create("Hello", "1.0")
.withPayload(payload) // JsonObject / Map / POJO
.withConfig(configManager) // stamps tags + the UNS identity
.build();
messaging.publish("itest/pubsub", msg);
// request/reply — ReplyFuture extends CompletableFuture<Message>;
// a framework deadline (default 30 s) bounds the wait
ReplyFuture future = messaging.request("itest/request", request);
Message reply = future.get();
// responder side:
messaging.reply(request, replyMsg); // copies correlation id