Skip to content

Commit b4c922b

Browse files
authored
Merge pull request #2486 from beyonnex-io/feature/serializer-message-categories-and-kamon-metric-cache
Categorize cluster (de)serialization metrics and drop per-op allocation in metric wrappers
2 parents c548aab + cc56ff8 commit b4c922b

6 files changed

Lines changed: 295 additions & 14 deletions

File tree

internal/utils/cluster/src/main/java/org/eclipse/ditto/internal/utils/cluster/AbstractJsonifiableWithDittoHeadersSerializer.java

Lines changed: 114 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@
2121
import java.nio.charset.Charset;
2222
import java.nio.charset.StandardCharsets;
2323
import java.text.MessageFormat;
24+
import java.util.EnumMap;
25+
import java.util.Map;
2426
import java.util.Optional;
27+
import java.util.concurrent.ConcurrentHashMap;
28+
import java.util.concurrent.ConcurrentMap;
2529
import java.util.function.Function;
2630

2731
import javax.annotation.Nullable;
@@ -40,8 +44,14 @@
4044
import org.eclipse.ditto.base.model.json.JsonSchemaVersion;
4145
import org.eclipse.ditto.base.model.json.Jsonifiable;
4246
import org.eclipse.ditto.base.model.signals.JsonParsable;
47+
import org.eclipse.ditto.base.model.signals.WithResource;
4348
import org.eclipse.ditto.base.model.signals.WithType;
49+
import org.eclipse.ditto.base.model.signals.acks.Acknowledgement;
50+
import org.eclipse.ditto.base.model.signals.acks.Acknowledgements;
51+
import org.eclipse.ditto.base.model.signals.announcements.Announcement;
4452
import org.eclipse.ditto.base.model.signals.commands.Command;
53+
import org.eclipse.ditto.base.model.signals.commands.CommandResponse;
54+
import org.eclipse.ditto.base.model.signals.events.Event;
4555
import org.eclipse.ditto.internal.utils.metrics.DittoMetrics;
4656
import org.eclipse.ditto.internal.utils.metrics.instruments.counter.Counter;
4757
import org.eclipse.ditto.internal.utils.metrics.instruments.tag.Tag;
@@ -90,14 +100,19 @@ public abstract class AbstractJsonifiableWithDittoHeadersSerializer extends Seri
90100

91101
private static final String METRIC_NAME_SUFFIX = "_serializer_messages";
92102
private static final String METRIC_DIRECTION = "direction";
103+
private static final String METRIC_CATEGORY = "category";
104+
private static final String METRIC_RESOURCE_TYPE = "resource_type";
105+
private static final String DIRECTION_IN = "in";
106+
private static final String DIRECTION_OUT = "out";
107+
private static final String RESOURCE_TYPE_OTHER = "other";
93108

94109
private final int identifier;
95110
private final MappingStrategies mappingStrategies;
96111
private final Function<Object, String> manifestProvider;
97112
private final BufferPool byteBufferPool;
98113
private final Long defaultBufferSize;
99-
private final Counter inCounter;
100-
private final Counter outCounter;
114+
private final Map<Category, ConcurrentMap<String, Counter>> inCounters;
115+
private final Map<Category, ConcurrentMap<String, Counter>> outCounters;
101116
private final String serializerName;
102117

103118
/**
@@ -127,10 +142,54 @@ protected AbstractJsonifiableWithDittoHeadersSerializer(
127142
final var maxPoolEntries = config.withFallback(FALLBACK_CONF).getInt(CONFIG_DIRECT_BUFFER_POOL_LIMIT);
128143
byteBufferPool = new DirectByteBufferPool(defaultBufferSize.intValue(), maxPoolEntries);
129144

130-
inCounter = DittoMetrics.counter(serializerName.toLowerCase() + METRIC_NAME_SUFFIX)
131-
.tag(METRIC_DIRECTION, "in");
132-
outCounter = DittoMetrics.counter(serializerName.toLowerCase() + METRIC_NAME_SUFFIX)
133-
.tag(METRIC_DIRECTION, "out");
145+
inCounters = newCategoryCounterCache();
146+
outCounters = newCategoryCounterCache();
147+
}
148+
149+
private static Map<Category, ConcurrentMap<String, Counter>> newCategoryCounterCache() {
150+
final Map<Category, ConcurrentMap<String, Counter>> cache = new EnumMap<>(Category.class);
151+
for (final Category category : Category.values()) {
152+
cache.put(category, new ConcurrentHashMap<>());
153+
}
154+
return cache;
155+
}
156+
157+
/**
158+
* Increments the {@code <serializer>_serializer_messages} counter tagged with the {@code direction}, the coarse
159+
* signal {@code category} and the {@code resource_type} of the passed {@code object}. The fully tagged counters
160+
* are cached per {@code (category, resource_type)} combination so that on the hot (de)serialization path only a map
161+
* lookup and the increment are performed; the tagged counter is built at most once per combination and serializer.
162+
*
163+
* @param countersByCategory the direction-specific counter cache ({@link #inCounters} or {@link #outCounters}).
164+
* @param direction the {@code direction} tag value ({@value #DIRECTION_IN} or {@value #DIRECTION_OUT}).
165+
* @param object the (de)serialized object to classify.
166+
*/
167+
private void incrementCounter(final Map<Category, ConcurrentMap<String, Counter>> countersByCategory,
168+
final String direction, final Object object) {
169+
170+
final var category = Category.of(object);
171+
final var resourceType = resourceTypeOf(object);
172+
final var countersByResourceType = countersByCategory.get(category);
173+
var counter = countersByResourceType.get(resourceType);
174+
if (null == counter) {
175+
// slow path, hit at most once per (category, resource_type) combination for a serializer's lifetime:
176+
counter = countersByResourceType.computeIfAbsent(resourceType,
177+
rt -> DittoMetrics.counter(serializerName.toLowerCase() + METRIC_NAME_SUFFIX)
178+
.tag(METRIC_DIRECTION, direction)
179+
.tag(METRIC_CATEGORY, category.getTag())
180+
.tag(METRIC_RESOURCE_TYPE, rt));
181+
}
182+
counter.increment();
183+
}
184+
185+
private static String resourceTypeOf(final Object object) {
186+
final String result;
187+
if (object instanceof WithResource withResource) {
188+
result = withResource.getResourceType();
189+
} else {
190+
result = RESOURCE_TYPE_OTHER;
191+
}
192+
return result;
134193
}
135194

136195
@Override
@@ -155,7 +214,7 @@ public void toBinary(final Object object, final ByteBuffer buf) {
155214
try {
156215
serializeIntoByteBuffer(jsonObject, buf);
157216
LOG.trace("toBinary jsonStr about to send 'out': {}", jsonObject);
158-
outCounter.increment();
217+
incrementCounter(outCounters, DIRECTION_OUT, object);
159218
} catch (final BufferOverflowException e) {
160219
final var errorMessage = MessageFormat.format(
161220
"Could not put bytes of JSON string <{0}> into ByteBuffer due to BufferOverflow",
@@ -282,7 +341,7 @@ public Object fromBinary(final ByteBuffer buf, final String manifest) {
282341
serializerName,
283342
BinaryToHexConverter.createDebugMessageByTryingToConvertToHexString(buf));
284343
}
285-
inCounter.increment();
344+
incrementCounter(inCounters, DIRECTION_IN, jsonifiable);
286345
return jsonifiable;
287346
} catch (final NotSerializableException e) {
288347
return e;
@@ -448,4 +507,51 @@ private static String getDefaultManifestOrThrow(final JsonObject jsonObject) thr
448507
.orElseThrow(() -> new NotSerializableException("No type found for inner JSON!"));
449508
}
450509

510+
/**
511+
* Coarse, low-cardinality classification of a (de)serialized message, used as the {@code category} metric tag to
512+
* understand the composition of cluster (de)serialization traffic - e.g. distinguishing event fan-out from command
513+
* load. It is combined with the {@code resource_type} tag (derived from {@link WithResource#getResourceType()}).
514+
*/
515+
private enum Category {
516+
517+
EVENT("event"),
518+
COMMAND("command"),
519+
RESPONSE("response"),
520+
ACKNOWLEDGEMENT("acknowledgement"),
521+
ANNOUNCEMENT("announcement"),
522+
ERROR("error"),
523+
OTHER("other");
524+
525+
private final String tag;
526+
527+
Category(final String tag) {
528+
this.tag = tag;
529+
}
530+
531+
private String getTag() {
532+
return tag;
533+
}
534+
535+
private static Category of(final Object object) {
536+
final Category result;
537+
// order matters: more specific types must be checked first (e.g. an Acknowledgement is a CommandResponse):
538+
if (object instanceof Event) {
539+
result = EVENT;
540+
} else if (object instanceof Announcement) {
541+
result = ANNOUNCEMENT;
542+
} else if (object instanceof Acknowledgement || object instanceof Acknowledgements) {
543+
result = ACKNOWLEDGEMENT;
544+
} else if (object instanceof CommandResponse) {
545+
result = RESPONSE;
546+
} else if (object instanceof Command) {
547+
result = COMMAND;
548+
} else if (object instanceof DittoRuntimeException) {
549+
result = ERROR;
550+
} else {
551+
result = OTHER;
552+
}
553+
return result;
554+
}
555+
}
556+
451557
}

internal/utils/cluster/src/test/java/org/eclipse/ditto/internal/utils/cluster/SharedJsonifiableSerializerTest.java

Lines changed: 119 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import static org.assertj.core.api.Assertions.assertThat;
1616

17+
import java.util.List;
1718
import java.util.Map;
1819

1920
import org.assertj.core.api.AutoCloseableSoftAssertions;
@@ -27,6 +28,10 @@
2728
import org.eclipse.ditto.base.model.signals.ShardedMessageEnvelope;
2829
import org.eclipse.ditto.base.model.signals.commands.GlobalCommandRegistry;
2930
import org.eclipse.ditto.base.model.signals.commands.GlobalCommandResponseRegistry;
31+
import org.eclipse.ditto.base.model.signals.events.GlobalEventRegistry;
32+
import org.eclipse.ditto.internal.utils.metrics.DittoMetrics;
33+
import org.eclipse.ditto.internal.utils.metrics.instruments.tag.Tag;
34+
import org.eclipse.ditto.internal.utils.metrics.instruments.tag.TagSet;
3035
import org.eclipse.ditto.internal.utils.tracing.DittoTracingInitResource;
3136
import org.eclipse.ditto.json.JsonObject;
3237
import org.eclipse.ditto.things.model.Thing;
@@ -35,6 +40,7 @@
3540
import org.eclipse.ditto.things.model.signals.commands.modify.CreateThing;
3641
import org.eclipse.ditto.things.model.signals.commands.modify.CreateThingResponse;
3742
import org.eclipse.ditto.things.model.signals.commands.query.RetrieveThings;
43+
import org.eclipse.ditto.things.model.signals.events.ThingCreated;
3844
import org.junit.AfterClass;
3945
import org.junit.Before;
4046
import org.junit.BeforeClass;
@@ -58,19 +64,25 @@ public final class SharedJsonifiableSerializerTest {
5864

5965
private static enum SerializerImplementation {
6066

61-
JSONIFIABLE_SERIALIZER {
67+
JSONIFIABLE_SERIALIZER("json_serializer_messages") {
6268
@Override
6369
public AbstractJsonifiableWithDittoHeadersSerializer getInstance(final ExtendedActorSystem actorSystem) {
6470
return new JsonJsonifiableSerializer(actorSystem);
6571
}
6672
},
67-
CBOR_JSONIFIABLE_SERIALIZER {
73+
CBOR_JSONIFIABLE_SERIALIZER("cbor_serializer_messages") {
6874
@Override
6975
public AbstractJsonifiableWithDittoHeadersSerializer getInstance(final ExtendedActorSystem actorSystem) {
7076
return new CborJsonifiableSerializer(actorSystem);
7177
}
7278
};
7379

80+
private final String metricName;
81+
82+
SerializerImplementation(final String metricName) {
83+
this.metricName = metricName;
84+
}
85+
7486
abstract AbstractJsonifiableWithDittoHeadersSerializer getInstance(ExtendedActorSystem actorSystem);
7587

7688
}
@@ -251,4 +263,109 @@ private static final class DittoHeadersStrategy extends MappingStrategies {
251263

252264
}
253265

266+
/**
267+
* Verifies that the {@code <serializer>_serializer_messages} counter is tagged with the expected
268+
* {@code direction}, coarse signal {@code category} and {@code resource_type} for the (de)serialized signal.
269+
*/
270+
@RunWith(Parameterized.class)
271+
public static final class CategoryAndResourceTypeTagTest {
272+
273+
private static final String TAG_DIRECTION = "direction";
274+
private static final String TAG_CATEGORY = "category";
275+
private static final String TAG_RESOURCE_TYPE = "resource_type";
276+
277+
@ClassRule
278+
public static final DittoTracingInitResource DITTO_TRACING_INIT_RESOURCE =
279+
DittoTracingInitResource.disableDittoTracing();
280+
281+
private static Thing thing;
282+
private static ExtendedActorSystem actorSystem;
283+
284+
@Parameterized.Parameter
285+
public SerializerImplementation serializerImplementation;
286+
287+
private AbstractJsonifiableWithDittoHeadersSerializer underTest;
288+
289+
@Parameterized.Parameters(name = "{0}")
290+
public static SerializerImplementation[] getSerializers() {
291+
return SerializerImplementation.values();
292+
}
293+
294+
@BeforeClass
295+
public static void setUpClass() {
296+
thing = Thing.newBuilder().setId(ThingId.generateRandom()).build();
297+
actorSystem = getActorSystem(ThingSignalsStrategy.class);
298+
}
299+
300+
@AfterClass
301+
public static void tearDownClass() {
302+
TestKit.shutdownActorSystem(actorSystem);
303+
}
304+
305+
@Before
306+
public void setUp() {
307+
underTest = serializerImplementation.getInstance(actorSystem);
308+
}
309+
310+
@Test
311+
public void thingCommandIsTaggedAsCommandOnThingResource() {
312+
assertRoundTripTaggedWith(CreateThing.of(thing, null, DITTO_HEADERS), "command", "thing");
313+
}
314+
315+
@Test
316+
public void thingCommandResponseIsTaggedAsResponseOnThingResource() {
317+
assertRoundTripTaggedWith(CreateThingResponse.of(thing, DITTO_HEADERS), "response", "thing");
318+
}
319+
320+
@Test
321+
public void thingEventIsTaggedAsEventOnThingResource() {
322+
assertRoundTripTaggedWith(ThingCreated.of(thing, 1L, null, DITTO_HEADERS, null), "event", "thing");
323+
}
324+
325+
private void assertRoundTripTaggedWith(final Object signal, final String expectedCategory,
326+
final String expectedResourceType) {
327+
328+
final long outBefore = counterCount("out", expectedCategory, expectedResourceType);
329+
final long inBefore = counterCount("in", expectedCategory, expectedResourceType);
330+
331+
final byte[] serialized = underTest.toBinary(signal);
332+
final Object deserialized = underTest.fromBinary(serialized, underTest.manifest(signal));
333+
334+
try (final AutoCloseableSoftAssertions softly = new AutoCloseableSoftAssertions()) {
335+
softly.assertThat(deserialized)
336+
.as("round-trip result")
337+
.isEqualTo(signal);
338+
softly.assertThat(counterCount("out", expectedCategory, expectedResourceType))
339+
.as("'out' count for category=<%s>, resource_type=<%s>", expectedCategory, expectedResourceType)
340+
.isEqualTo(outBefore + 1);
341+
softly.assertThat(counterCount("in", expectedCategory, expectedResourceType))
342+
.as("'in' count for category=<%s>, resource_type=<%s>", expectedCategory, expectedResourceType)
343+
.isEqualTo(inBefore + 1);
344+
}
345+
}
346+
347+
private long counterCount(final String direction, final String category, final String resourceType) {
348+
final TagSet tags = TagSet.ofTagCollection(List.of(
349+
Tag.of(TAG_DIRECTION, direction),
350+
Tag.of(TAG_CATEGORY, category),
351+
Tag.of(TAG_RESOURCE_TYPE, resourceType)));
352+
return DittoMetrics.counter(serializerImplementation.metricName, tags).getCount();
353+
}
354+
355+
private static final class ThingSignalsStrategy extends MappingStrategies {
356+
357+
ThingSignalsStrategy() {
358+
super(MappingStrategiesBuilder.newInstance()
359+
.add(GlobalErrorRegistry.getInstance())
360+
.add(GlobalCommandRegistry.getInstance())
361+
.add(GlobalCommandResponseRegistry.getInstance())
362+
.add(GlobalEventRegistry.getInstance())
363+
.add(Thing.class, ThingsModelFactory::newThing)
364+
.build());
365+
}
366+
367+
}
368+
369+
}
370+
254371
}

internal/utils/metrics/src/main/java/org/eclipse/ditto/internal/utils/metrics/instruments/counter/KamonCounter.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ public final class KamonCounter implements Counter {
3737
private final String name;
3838
private final TagSet tags;
3939

40+
/**
41+
* Memoized resolved Kamon counter. {@code name} and {@code tags} are immutable (and {@link #tag(Tag)} /
42+
* {@link #tags(TagSet)} return new instances), so the resolved instrument is invariant and can be cached to avoid
43+
* re-resolving it (registry lookup + tag-set conversion) on every {@link #increment()}.
44+
*/
45+
@SuppressWarnings("squid:S3077")
46+
private transient volatile kamon.metric.Counter kamonInternalCounter;
47+
4048
private KamonCounter(final String name, final TagSet tags) {
4149
this.name = argumentNotEmpty(name, "name");
4250
this.tags = checkNotNull(tags, "tags");
@@ -102,7 +110,13 @@ private long getSnapshot() {
102110
}
103111

104112
private kamon.metric.Counter getKamonInternalCounter() {
105-
return Kamon.counter(name).withTags(KamonTagSetConverter.getKamonTagSet(tags));
113+
kamon.metric.Counter result = kamonInternalCounter;
114+
if (null == result) {
115+
// benign race: for a given (name, tags) Kamon returns the same instrument, so a redundant resolve is safe:
116+
result = Kamon.counter(name).withTags(KamonTagSetConverter.getKamonTagSet(tags));
117+
kamonInternalCounter = result;
118+
}
119+
return result;
106120
}
107121

108122
@Override

0 commit comments

Comments
 (0)