Repository navigation
Introduce ObjectStore backed by global weak map - #55
Conversation
amarziali
left a comment
There was a problem hiding this comment.
looks good. I left a couple of minor/question
f278a4e to
5ac9103
Compare
PerfectSlayer
left a comment
There was a problem hiding this comment.
I left a bunch of comments, mostly about API, but it looks good to me 👍
🎯 suggestion: Quick feedback about the store API: I found the getOrPut() and getOrCompute() a bit surprising -- I hard to read through the Javadoc twice to make sure I get the expected behavior.
What about using the API similar the Java map instead: computeIfAbsent()? And the get counterpart can become putIfAbstent(). Do you think it can make it more explicit?
💭 thought: I left few comments about null checks, do you think adding a package-info.java with @ParametersAreNonnullByDefault would API users and their IDE?
| /** Manages unique {@link ObjectStore} ids for each key-value type combination. */ | ||
| public final class ObjectStoreIds { | ||
|
|
||
| private static final Map<String, Short> typeIds = new ConcurrentHashMap<>(); |
There was a problem hiding this comment.
🔍 nitpick: Java naming conventions -- help with code reviews 🙏
| private static final Map<String, Short> typeIds = new ConcurrentHashMap<>(); | |
| private static final Map<String, Short> TYPE_IDS = new ConcurrentHashMap<>(); |
| private static final Map<String, Short> typeIds = new ConcurrentHashMap<>(); | ||
|
|
||
| // start at 1 to give a range of store ids we can allocate another way (0-32767) | ||
| private static final AtomicInteger nextTypeId = new AtomicInteger(1); |
There was a problem hiding this comment.
🔍 nitpick: Java naming conventions -- help with code reviews 🙏
| private static final AtomicInteger nextTypeId = new AtomicInteger(1); | |
| private static final AtomicInteger NEXT_TYPE_ID = new AtomicInteger(1); |
There was a problem hiding this comment.
Do we need to distinguish between a constant, i.e. a static immutable value, and a field that happens to be static but actually holds mutable state? For the latter camel-case is probably more appropriate to hint that the contained value is mutable. I'm leaning towards that direction, but will make sure this code-base is consistent one way or another.
There was a problem hiding this comment.
FWIW the old Java style guide stated:
Names of fields being used as constants should be all upper-case, with underscores separating words.
The following are considered to be constants:
- All static final primitive types (Remember that all interface fields are inherently static final).
- All static final object reference types that are never followed by "." (dot).
- All static final arrays that are never followed by "[" (opening square bracket)
Currently this code-base is inconsistent - I'll make it consistent and document the naming convention used in this project
| * @param <V> the value type | ||
| * @return the key-value's object-store | ||
| */ | ||
| public static <K, V> ObjectStore<K, V> of(Class<K> keyType, Class<V> valueType) { |
There was a problem hiding this comment.
❔ question: Should we handle the null values here? It might not be too expensive to do it here 🤷
There was a problem hiding this comment.
I'll make sure that the exception is some form of IllegalArgumentException
Note this deliberately uses
This difference in behaviour means you need additional logic to adapt the true behaviour of Note that |
Make sense! Thanks for the clarification 🙏 |
5ac9103 to
d7c8600
Compare
d7c8600 to
e89aee7
Compare

What Does This Do
Introduce ObjectStore API + initial implementation backed by a single global weak map.
Motivation
This the first of a series of PRs to implement field-injection / object-storage outside of tracing. The idea here is to use a single global weak map when field-injection is not available / possible.
Store ids are encoded by allocating each type a unique code and combining the key and value type to get a unique store id. This store id is added to the weak-reference used to track the 'holding' object in the global map. This lets us have a single global map, rather than maintain a separate map per-store - at the cost of adding the store-id to each key. (Note the global map approach used here doesn't require that key-value combinations are assigned sequential ids, just that they are unique.)
Lookups use a non-reference type which has a symmetric
hashCode/equalsto the reference type. This avoids the cost of reference tracking for purely read-only requests. Lookup keys are reusable and cached per-thread to avoid allocations.The global map has a hard limit of 100_000 entries and a soft limit of 50_000. The soft limit is used as a sign to start proactively removing old entries, which are likely unused but may be still in the map because of a bug in external code using the object-store. This mitigates against a rogue caller using up all the entries. (Also note that the weak store is itself a fall-back for when field-injection was not possible, and is not expected to be heavily used.)
To limit overhead, old entries are identified by a periodic sampling approach whenever
removeStaleEntriesis called. We therefore don't guarantee that it's always the oldest content that is removed when over the soft limit, but it won't be any of the keys added since the last call toremoveStaleEntries.Once the global store has grown over a certain size we also attempt inline removal of stale entries, using a one-out one-in approach. This is meant to handle situations where the periodic call to
removeStaleEntriesis not enough to handle churn in the map.Lastly the
GlobalObjectStoretype is deliberately public to help migrate the existing field-injection approach in the tracer while the local field-injection feature is completed.Additional Notes
The unit tests were developed with the help of Claude
Benchmark Results
The single global map approach performs the same as having a map per-store for most occupancy levels. It handles higher occupancy levels better.