Skip to content

Messaging API

This reference covers the principal public messaging APIs in each language. Signature blocks are selected excerpts; they do not reproduce every overload or metadata accessor. For the conceptual walkthrough (transports, wire envelope, concurrency caps and broker config), read the Messaging guide first.

Java is canonical; Python, Rust, and TypeScript mirror its shared contract. 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 ReceiveMode — RECEIVE_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. Java/Python treat <= 0 as uncapped; Rust/TypeScript clamp that value 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.

Normal MQTT and Greengrass IPC messages carry the shared protobuf EdgeCommonsMessage envelope. Its JSON projection has header, optional identity and tags, and body, with optional content type, content encoding and schema metadata. Projection header keys are snake_case; timestamp_ms is the epoch-millisecond timestamp carried by protobuf. Config-bound builders stamp the publisher identity. Component scope omits identity.instance.

Normal subscriptions use the byte decoders below. Foreign JSON, arbitrary bytes and malformed protobuf are rejected, not converted to a synthetic raw message. JSON object helpers remain useful for application tooling and explicit raw publication, but are separate from transport decoding.

Bodies can be typed standard messages, dynamic structured values, or opaque bytes. The bounded opaque-body APIs support 64 KiB. In JSON-facing body views, native bytes may appear as an _edgecommonsBinary base64 marker; protobuf carries the bytes directly. Binary accessors recover the original bytes. Diagnostic JSON may replace opaque content with a length/hash summary, so it is not a general lossless import/export format.

The following are selected API excerpts. Message metadata also includes content type, content encoding, schema and body case; use the language’s accessors when those distinctions matter.

Message exposes accessors; use MessageBuilder for construction.

byte[] toBytes(); // protobuf transport encoder
static Message fromBytes(byte[] bytes); // protobuf transport decoder
JsonObject toDict(); // JSON-facing projection
JsonObject toDiagnosticJson(); // diagnostic projection (opaque content is summarized)
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 not binary; validates the 64 KiB limit
byte[] getOpaqueBody();
String getContentType();
String getContentEncoding();
MessageBodySchema getSchema();
MessageBodyCase getBodyCase();
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. timestamp_ms is the canonical protobuf epoch-millisecond value; timestamp is its RFC3339 projection/API representation. 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)
long timestampMs;
String correlationId; String uuid; String replyTo;
static final String REPLY_MESSAGE_TOPIC_PREFIX = "edgecommons/reply-";
JsonObject toDict(); // projection includes timestamp_ms, 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):

  • local — host (string), port (int), clientId (string), optional qos and credentials.
  • northbound — host 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.
  • credentials — caPath, 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