Skip to content

SolaceIO: map message user properties - #40108

Open
ngibanel wants to merge 1 commit into
apache:masterfrom
ngibanel:feature/solace-user-properties
Open

ngibanel wants to merge 1 commit into
apache:masterfrom
ngibanel:feature/solace-user-properties

Conversation

@ngibanel

Copy link
Copy Markdown
Contributor

Summary

SolaceIO now supports user properties (message metadata) mapping in both the read and the
write direction.

Fixes #40099

Motivation

The SolaceIO serialization layer (Solace.SolaceRecordMapper) currently drops the JCSMP user
property map (XMLMessage.getProperties(), an SDTMap) in both directions:

  • Read: toRecord does not map getProperties() into Solace.Record, so all
    header-level metadata carried by the message is lost when reading from Solace.
  • Write: toMessage never calls setProperties(SDTMap), so it is impossible to publish
    user properties from a Beam pipeline.

User properties are the recommended way to carry small header-level metadata fields alongside
the payload. They are also the mechanism that interoperates across protocols: Solace translates
the SDTMap user property map to/from MQTT 5 user properties and AMQP application properties.
Supporting them enables cross-protocol (MQTT 5 / AMQP / SMF) metadata interoperability from
Beam pipelines.

This is a follow-up to #39875, reusing the same mapper/protocol layer introduced for
payload type support.

Changes

  • Solace.Record gains a userProperties field of type Map<String, String> (schema field 14).
    It defaults to an empty map and is never null, so application code does not need null handling.
  • Deserialization (toRecord): each entry of the JCSMP SDTMap is stringified
    (String.valueOf) into the record. Entries with a null value are skipped with a warning;
    entries that cannot be read are logged and skipped. A message without user properties maps
    to an empty map.
  • Serialization (toMessage): when the record carries a non-empty userProperties map,
    it is published as a JCSMP SDTMap via setProperties(...). When the map is empty,
    setProperties is not called, so published messages are identical to the previous behavior.
  • The string-valued map is a deliberate, accepted trade-off aligned with MQTT 5, where user
    properties are strings. Users needing the original non-string types can cast the values back
    in their pipeline code. Typed SDT values are out of scope.

Thank you for your contribution! Follow this checklist to help us incorporate your contribution quickly and easily:

  • Mention the appropriate issue in your description (for example: addresses #123), if applicable. This will automatically add a link to the pull request in the issue. If you would like the issue to automatically close on merging the pull request, comment fixes #<ISSUE NUMBER> instead.
  • Update CHANGES.md with noteworthy changes.
  • If this contribution is large, please file an Apache Individual Contributor License Agreement.

See the Contributor Guide for more tips on how to make review process smoother.

@ngibanel
ngibanel force-pushed the feature/solace-user-properties branch 3 times, most recently from 51b798d to ac66e7b Compare September 13, 2026 11:19
@ngibanel
ngibanel force-pushed the feature/solace-user-properties branch from ac66e7b to c75a283 Compare September 13, 2026 17:16
@codecov

codecov Bot commented Sep 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 58.41%. Comparing base (790468a) to head (c75a283).

Additional details and impacted files
@@             Coverage Diff              @@
##             master   #40108      +/-   ##
============================================
- Coverage     58.72%   58.41%   -0.32%     
+ Complexity    15295    13571    -1724     
============================================
  Files          2791     2577     -214     
  Lines        278417   269098    -9319     
  Branches      12320    11058    -1262     
============================================
- Hits         163512   157183    -6329     
+ Misses       108500   105961    -2539     
+ Partials       6405     5954     -451     
Flag Coverage Δ
java 64.60% <ø> (-0.26%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ngibanel
ngibanel marked this pull request as ready for review September 14, 2026 08:27
@ngibanel

ngibanel commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

R: @stankiewicz
could you please review this PR. Thanks!

@github-actions

Copy link
Copy Markdown
Contributor

Stopping reviewer notifications for this pull request: review requested by someone other than the bot, ceding control. If you'd like to restart, comment assign set of reviewers

@stankiewicz stankiewicz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Nicolas, requested few changes, ptal!

}
return String.valueOf(value);
} catch (SDTException e) {
LOG.error("Could not read user property '{}'.", key, e);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ngibanel this will cause metadata loss as message will be acked. Maybe rethrowing will be better?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, that means the consumer must enable the dead message queue in Solace (which is a best practice) to avoid losing messages if metadata cannot be deserialized. Otherwise the broker will redeliver the message until the max retry count is reached and then message will be discarded and lost.

if (value == null) {
return null;
}
return String.valueOf(value);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will invoke toString but some types (destination, stream, byte array) that are part of SDTMap don't have it and this will run poorly for those, some specialized approach should be used for those like
Destination type maybe getName() should be invoked and for Stream maybe byte array and for byte array you should somehow preserve those bytes so its' not becoming garbage

@ngibanel ngibanel Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch I didn't check all the types. Maybe translating all types into string is not the good choice at the end because with this design we won't be able to reverse to Solace types.

@stankiewicz what do you think if instead having a Map<String, String>, having a Map<String, UserPropertyValue> where UserPropertyValue will be a beam schema compatible model that supports all the type kinds :

  @AutoValue
  @DefaultSchema(AutoValueSchema.class)
  public abstract static class UserPropertyValue {
    public enum Kind {
      BOOLEAN,
      BYTE,
      SHORT,
      INTEGER,
      LONG,
      FLOAT,
      DOUBLE,
      CHARACTER,
      STRING,
      BYTES,
      TOPIC,
      QUEUE,
      MAP,
      STREAM
    }

    public abstract Kind getKind();

    public abstract @Nullable Map<String, UserPropertyValue> getMapValue();

    public abstract @Nullable List<UserPropertyValue> getStreamValue();
    
    ...
 }

for (String key : properties.keySet()) {
String value = stringifyUserProperty(properties, key);
if (value == null) {
LOG.warn("User property '{}' has a null value, skipping.", key);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this may be excessive, would skip this log.

BytesXMLMessage message = JCSMPFactory.onlyInstance().createBytesXMLMessage();
message.setApplicationMessageId("id");
SDTMap properties = JCSMPFactory.onlyInstance().createMap();
properties.putString("contentType", "application/json");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cover all SDTMap types

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request]: SolaceIO: Support user properties (message metadata) mapping

2 participants