From b747cf75a189869dd85347eda8b9e5fc01c97d99 Mon Sep 17 00:00:00 2001 From: Neil Clifford Date: Fri, 29 Sep 2017 15:55:33 +0100 Subject: [PATCH] Initial re-work of documentation --- docs/CM_Tutorial_Behaviour.adoc | 339 ++++++++++++++++++++++++++++++++ 1 file changed, 339 insertions(+) create mode 100755 docs/CM_Tutorial_Behaviour.adoc diff --git a/docs/CM_Tutorial_Behaviour.adoc b/docs/CM_Tutorial_Behaviour.adoc new file mode 100755 index 000000000..0db2c6ab9 --- /dev/null +++ b/docs/CM_Tutorial_Behaviour.adoc @@ -0,0 +1,339 @@ += 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 via this configured SPI. + + - `ChronicleMapBuilder.mapMethods()` is the higher-level of listening for local calls of Map + methods. Methods in `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 the three tiers (or the two bottom): + + 1. Query tier: `MapQueryContext` interface + 2. Entry tier: `MapEntry` and `MapAbsentEntry` interfaces + 3. Data tier: `Data` interface + +`MapMethods` and `MapRemoteOperations` methods accept *query context*, i. e. these SPI is above +the Query tier. `MapEntryOperations` methods accept `MapEntry` or `MapAbsentEntry`, i. e. this SPI +is between Query and Entry tiers. + +Combined, interception SPI interfaces and `ChronicleMap.queryContext()` API are powerful enough to + + - Log all operations of some kind on `ChronicleMap` (e. g. all remove, insert or update operations) + - Log some specific operations on `ChronicleMap` (e. g. log only acquireUsing() calls, which has + created a new entry) + - Forbid performing operations of some kind on the `ChronicleMap` instance + - Backup all changes to `ChronicleMap` to some alternative storage, e. g. 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 concurrent environment, by + acquiring locks on all keys before updating the entries + - Define 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 + +Just 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 bidirectional map (i. e. 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())); + } + } + } +``` +