= Behaviour Customization Neil Clifford :toc: macro :toclevels: 2 :css-signature: demo :toc-placement: macro :icons: font toc::[] == Customization You could customize `ChronicleMap` behaviour on several levels: - `ChronicleMapBuilder.entryOperations()` define the "inner" listening level. All operations with entries, either during ordinary map method calls, remote calls, replication or modifications during iteration over the map, operate using this configured SPI. - `ChronicleMapBuilder.mapMethods()` is the higher-level of listening for local calls of Map methods. Methods in the `MapMethods` interface correspond to `Map` interface methods with the same names, and define their implementations for `ChronicleMap`. - `ChronicleMapBuilder.remoteOperations()` is for listening and customizing behaviour of remote calls, and replication events. All executions around `ChronicleMap` go through all three tiers, or just the last two): 1. Query tier: `MapQueryContext` interface 2. Entry tier: `MapEntry` and `MapAbsentEntry` interfaces 3. Data tier: `Data` interface `MapMethods` and `MapRemoteOperations` methods accept *query context*; SPI is above the Query tier. `MapEntryOperations` methods accept `MapEntry` or `MapAbsentEntry`; SPI is between Query and Entry tiers. When combined, interception SPI interfaces and the `ChronicleMap.queryContext()` API are powerful enough to: - Log all operations of some kind on `ChronicleMap` (for example, all remove, insert, or update operations). - Log some specific operations on `ChronicleMap` (for example, log only `acquireUsing()` calls, which has created a new entry). - Inhibit operations of some kind on the `ChronicleMap` instance. - Backup all changes to `ChronicleMap` to some alternative storage; for example, an SQL database. - Perform multi-Chronicle Map operations correctly in concurrent environment, by acquiring locks on all ChronicleMaps, before updating them. - Perform multi-key operations on a single `ChronicleMap` correctly in a concurrent environment, by acquiring locks on all keys before updating the entries. - Define your own replication/reconciliation logic for distributed Chronicle Maps. - Dump statistics of the Chronicle Map instance; each segment's load, size in bytes of each entry, etc. == Example - Simple logging To log all modification operations on `ChronicleMap`: ```java class SimpleLoggingMapEntryOperations implements MapEntryOperations { private static final SimpleLoggingMapEntryOperations INSTANCE = new SimpleLoggingMapEntryOperations(); public static MapEntryOperations simpleLoggingMapEntryOperations() { return SimpleLoggingMapEntryOperations.INSTANCE; } private SimpleLoggingMapEntryOperations() {} @Override public Void remove(@NotNull MapEntry entry) { System.out.println("remove " + entry.key() + ": " + entry.value()); entry.doRemove(); return null; } @Override public Void replaceValue(@NotNull MapEntry entry, Data newValue) { System.out.println("replace " + entry.key() + ": " + entry.value() + " -> " + newValue); entry.doReplaceValue(newValue); return null; } @Override public Void insert(@NotNull MapAbsentEntry absentEntry, Data value) { System.out.println("insert " + absentEntry.absentKey() + " -> " + value); absentEntry.doInsert(value); return null; } } ``` Usage: ```java ChronicleMap map = ChronicleMap .of(Integer.class, IntValue.class) .entries(100) .entryOperations(simpleLoggingMapEntryOperations()) .create(); // do anything with the map ``` == Example - BiMap Possible bi-directional map (that is, a map that preserves the uniqueness of its values, as well as that of its keys) implementation over Chronicle Maps. ```java enum DualLockSuccess {SUCCESS, FAIL} ``` ```java class BiMapMethods implements MapMethods { @Override public void remove(MapQueryContext q, ReturnValue returnValue) { while (true) { q.updateLock().lock(); try { MapEntry entry = q.entry(); if (entry != null) { returnValue.returnValue(entry.value()); if (q.remove(entry) == SUCCESS) return; } } finally { q.readLock().unlock(); } } } @Override public void put(MapQueryContext q, Data value, ReturnValue returnValue) { while (true) { q.updateLock().lock(); try { MapEntry entry = q.entry(); if (entry != null) { throw new IllegalStateException(); } else { if (q.insert(q.absentEntry(), value) == SUCCESS) return; } } finally { q.readLock().unlock(); } } } @Override public void putIfAbsent(MapQueryContext q, Data value, ReturnValue returnValue) { while (true) { try { if (q.readLock().tryLock()) { MapEntry entry = q.entry(); if (entry != null) { returnValue.returnValue(entry.value()); return; } // Key is absent q.readLock().unlock(); } q.updateLock().lock(); MapEntry entry = q.entry(); if (entry != null) { returnValue.returnValue(entry.value()); return; } // Key is absent if (q.insert(q.absentEntry(), value) == SUCCESS) return; } finally { q.readLock().unlock(); } } } @Override public boolean remove(MapQueryContext q, Data value) { while (true) { q.updateLock().lock(); MapEntry entry = q.entry(); try { if (entry != null && bytesEquivalent(entry.value(), value)) { if (q.remove(entry) == SUCCESS) { return true; } else { //noinspection UnnecessaryContinue continue; } } else { return false; } } finally { q.readLock().unlock(); } } } @Override public void acquireUsing(MapQueryContext q, ReturnValue returnValue) { throw new UnsupportedOperationException(); } @Override public void replace(MapQueryContext q, Data value, ReturnValue returnValue) { throw new UnsupportedOperationException(); } @Override public boolean replace(MapQueryContext q, Data oldValue, Data newValue) { throw new UnsupportedOperationException(); } @Override public void compute(MapQueryContext q, BiFunction remappingFunction, ReturnValue returnValue) { throw new UnsupportedOperationException(); } @Override public void merge(MapQueryContext q, Data value, BiFunction remappingFunction, ReturnValue returnValue) { throw new UnsupportedOperationException(); } } ``` ```java class BiMapEntryOperations implements MapEntryOperations { ChronicleMap reverse; public void setReverse(ChronicleMap reverse) { this.reverse = reverse; } @Override public DualLockSuccess remove(@NotNull MapEntry entry) { try (ExternalMapQueryContext rq = reverse.queryContext(entry.value())) { if (!rq.updateLock().tryLock()) { if (entry.context() instanceof MapQueryContext) return FAIL; throw new IllegalStateException("Concurrent modifications to reverse map " + "during remove during iteration"); } MapEntry reverseEntry = rq.entry(); if (reverseEntry != null) { entry.doRemove(); reverseEntry.doRemove(); return SUCCESS; } else { throw new IllegalStateException(entry.key() + " maps to " + entry.value() + ", but in the reverse map this value is absent"); } } } @Override public DualLockSuccess replaceValue(@NotNull MapEntry entry, Data newValue) { throw new UnsupportedOperationException(); } @Override public DualLockSuccess insert(@NotNull MapAbsentEntry absentEntry, Data value) { try (ExternalMapQueryContext rq = reverse.queryContext(value)) { if (!rq.updateLock().tryLock()) return FAIL; MapAbsentEntry reverseAbsentEntry = rq.absentEntry(); if (reverseAbsentEntry != null) { absentEntry.doInsert(value); reverseAbsentEntry.doInsert(absentEntry.absentKey()); return SUCCESS; } else { Data reverseKey = rq.entry().value(); if (reverseKey.equals(absentEntry.absentKey())) { // recover absentEntry.doInsert(value); return SUCCESS; } throw new IllegalArgumentException("Try to associate " + absentEntry.absentKey() + " with " + value + ", but in the reverse " + "map this value already maps to " + reverseKey); } } } } ``` Usage: ```java BiMapEntryOperations biMapOps1 = new BiMapEntryOperations<>(); ChronicleMap map1 = ChronicleMapBuilder .of(Integer.class, CharSequence.class) .name("direct-bimap") .entries(100) .actualSegments(1) .averageValueSize(10) .entryOperations(biMapOps1) .mapMethods(new BiMapMethods<>()) .create(); BiMapEntryOperations biMapOps2 = new BiMapEntryOperations<>(); ChronicleMap map2 = ChronicleMapBuilder .of(CharSequence.class, Integer.class) .name("reverse-bimap") .entries(100) .actualSegments(1) .averageKeySize(10) .entryOperations(biMapOps2) .mapMethods(new BiMapMethods<>()) .create(); biMapOps1.setReverse(map2); biMapOps2.setReverse(map1); map1.put(1, "1"); System.out.println(map2.get("1")); ``` == Example - Monitor Chronicle Map Statistics ```java public static void printMapStats(ChronicleMap map) { for (int i = 0; i < map.segments(); i++) { try (MapSegmentContext c = map.segmentContext(i)) { System.out.printf("segment %d contains %d entries\n", i, c.size()); c.forEachSegmentEntry(e -> System.out.printf("%s, %d bytes -> %s, %d bytes\n", e.key(), e.key().size(), e.value(), e.value().size())); } } } ``` ''' <>