Repository navigation
Replies: 1 comment
|
Hi Ceki, My preference would be to keep The MDC contract here is a snapshot. A wrapping map is only safe after copying into a I would also avoid a size-based switch unless a benchmark shows a meaningful benefit in a real workload. It introduces a magic threshold, exposes two runtime implementations, and expands the hardened-deserialization surface. The linear lookup cost is likely insignificant for typical MDC cardinalities, while the allocation savings happen on every MDC mutation. A small JMH benchmark across 1, 4, 8, 16, and 32 entries would be useful: keyed Best Regards, |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Discussion: unmodifiable MDC property map
This note is an invitation to discuss the change in commit 76c73d1 (
add MinimalUnmodifiableMap and return it in LogbackMDCAdapter#getPropertyMap) and whethergetPropertyMap()should ever returnUnmodifiableWrappingMapinstead of, or in addition to,MinimalUnmodifiableMap.Comments and counter-arguments are welcome.
What changed
LogbackMDCAdapter.getPropertyMap()used to return:It now returns:
The intended contract is the same: an unmodifiable
Map<String, String>snapshot. Laterput/removeon the thread's MDC must not change a map already handed out. The snapshot is cached in a ThreadLocal until the next write.MinimalUnmodifiableMapis a publicMap<String, String>. It copies entries into two parallelStringarrays (keys and values).getis a linear onindexOfmethod calls. It is listed on theHardenedLoggingEventInputStreamwhitelist. Serialization writes length plus eachString, not a raw array type.The same commit also adds
UnmodifiableWrappingMap. It is not used on thegetPropertyMap()path today. Its role is discussed below.Why not
Collections.unmodifiableMapas the public type?Collections.unmodifiableMap(Map)is the JDK factory. The concrete type it returns isjava.util.Collections.UnmodifiableMap, a private (package-private) nested class ofjava.util.Collections. Callers cannot subclass it, or invoke its constructor.The main reason for leaving that type behind is decoding. A private unmodifiable map cannot be reconstructed if the decoder needs to add entries piecemeal via
put().putthrowsUnsupportedOperationException, and there is no public constructor to wrap a map that the decoder has already filled. Frameworks that instantiate the runtime class and populate it withput(or that cannot callCollections.unmodifiableMapbecause they only see the private class name) fail.That is the primary motivation for
MinimalUnmodifiableMapinstead of shippingCollections$UnmodifiableMapas the MDC snapshot type.Owning a public class also makes a serialization whitelist entry and tests straightforward. Those are secondary to the decode/
putproblem.UnmodifiableWrappingMapas a public stand-inUnmodifiableWrappingMap<K, V>was added to solve the same decode problem without forcing the array layout.It is a public, generic unmodifiable view over an existing map, in the same spirit as the private
Collections.UnmodifiableMap:UnmodifiableWrappingMap(Map<? extends K, ? extends V> m)(null rejected);get,size,containsKey, …) delegate to the backing map;put/remove/clearthrowUnsupportedOperationException;entrySet()wraps the backing set soEntry.setValuecannot mutate the backing map.A decoder can therefore:
HashMap;putentries one by one;new UnmodifiableWrappingMap<>(map).That sequence is impossible with
Collections.UnmodifiableMapitself, because the class is private.The wrapper does not copy. Mutating the backing map remains visible through the view. Callers that need a snapshot must pass a copy, for example
new UnmodifiableWrappingMap<>(new HashMap<>(current)).MinimalUnmodifiableMapvsUnmodifiableWrappingMapBoth are public, unmodifiable maps we own. They differ in layout and in who is responsible for the snapshot.
MinimalUnmodifiableMapUnmodifiableWrappingMap<K,V>Map<String, String>onlyK,VString[]keys andString[]valuesgetindexOf)HashMap.getwhen the backing map is aHashMap)entrySetentrySet; eachnext()is an immutable entryemptyMap()putputputCollections.unmodifiableMapwhen a view is enoughOn
getPropertyMap(), wrapping the live read-writeHashMapwould break snapshot isolation. Using wrapping there would still mean:That is a full
HashMapcopy (table +Nodes) plus a wrapper — the same shape as the oldCollections.unmodifiableMap(new HashMap<>(...))code, with a public wrapper type.MinimalUnmodifiableMap(current)copies once into two compact arrays: one object + two arrays, noHashMaptable, noNodes.MDCConverterusesget(key)when a key is configured (%X{foo}) andentrySet()when it is not (%X). Neither requires hashing at typical MDC sizes (a handful of keys: request id, user, tenant).Size-based choice?
Should
getPropertyMap()returnMinimalUnmodifiableMapfor very small maps andUnmodifiableWrappingMap(over aHashMapcopy) otherwise?This is the question we would like to discuss.
A possible argument for a switch: if some applications put tens of MDC keys and use keyed
%X{key}on a hot logger, lineargetcould lose toHashMap.get. ArrayMap-style structures often lose to hashing only past roughly 8–16 keys.A possible argument against:
O(1)getafter aHashMapcopy, which is heavier to build than twoString[]s for realistic MDC sizes.n, that GC cost likely dominates any lookup difference. Full-map iteration allocates an entry object per key in both implementations.Kis a magic number. Callers would see two runtime types. Hardened deserialization would needUnmodifiableWrappingMap(and its backingHashMap) on the whitelist as well.Is there a workload where large MDCs plus keyed lookup would justify the extra path? If so, what threshold, and should both types stay on the hardened whitelist?
Open points
Collections$UnmodifiableMapwith a public type the right fix for piecemealputdecoding, and doesUnmodifiableWrappingMapcover that case well enough for non-MDC maps?MinimalUnmodifiableMappreferable to wrapping aHashMapcopy for all sizes, or only below somen?getPropertyMap()ever returnUnmodifiableWrappingMap, or should that class remain a general-purpose view unused on this path?Please weigh in.
All reactions