-
Notifications
You must be signed in to change notification settings - Fork 478
Expand file tree
/
Copy pathHashQueryContext.java
More file actions
177 lines (171 loc) · 7.8 KB
/
Copy pathHashQueryContext.java
File metadata and controls
177 lines (171 loc) · 7.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
/*
* Copyright 2013-2026 chronicle.software; SPDX-License-Identifier: Apache-2.0
*/
package net.openhft.chronicle.hash;
import net.openhft.chronicle.hash.locks.InterProcessLock;
import net.openhft.chronicle.hash.locks.InterProcessReadWriteUpdateLock;
import net.openhft.chronicle.map.MapEntryOperations;
import net.openhft.chronicle.map.MapMethods;
import net.openhft.chronicle.map.MapQueryContext;
import net.openhft.chronicle.set.SetEntryOperations;
import net.openhft.chronicle.set.SetQueryContext;
import org.jetbrains.annotations.Nullable;
/**
* Context of {@link ChronicleHash} operations with <i>individual keys</i>.
* <p>
* This context provides access to {@link InterProcessReadWriteUpdateLock}, governing access
* to the entry. Your have no chance to perform any actions under race with other threads, because
* all required locks are acquired automatically on each operation, you should deal with locks
* manually in the following cases:
* <ul>
* <li>You should perform some read operation, and then write. On first operation, the context
* will automatically acquire the read lock; on the second, it will try to acquire the update
* or write lock, but it will fail with {@link IllegalMonitorStateException}, because this is
* dead lock prone. (See {@link InterProcessReadWriteUpdateLock} documentation for explanation.
* So, such context code is incorrect: <pre>{@code
* // INCORRECT
* try (ExternalMapQueryContext<K, V, ?> q = map.queryContext(key)) {
* // q.entry(), checks if the entry is present in the map, and acquires
* // the read lock for that.
* MapEntry<K, V> entry = q.entry();
* if (entry != null) {
* // Tries to acquire the write lock to perform modification,
* // but this is an illegal upgrade: read -> write, throws IllegalMonitorStateException
* q.remove(entry);
* }
* }}</pre>
* So, to workaround this, you should acquire the {@linkplain #updateLock() update lock},
* which is upgradable to the write lock, <i>before</i> performing any reading in the context:
* <pre>{@code
* // CORRECT
* try (ExternalMapQueryContext<K, V, ?> q = map.queryContext(key)) {
* q.updateLock().lock(); // acquire the update lock before checking the entry presence.
* MapEntry<K, V> entry = q.entry();
* if (entry != null)
* q.remove(entry);
* }}</pre>
* </li>
* <li>You want to try to acquire some lock heuristically, in order to improve total {@code
* ChronicleHash} concurrency. You should base on probabilities of making some reading or
* writing operations. For example, see how {@link MapMethods#acquireUsing} might be
* implemented:<pre>{@code
* default void acquireUsing(MapQueryContext<K, V, R> q, ReturnValue<V> returnValue) {
* // For acquireUsing(), it is assumed to be very probable, that the entry is already
* // present in the map, so we will perform the whole acquireUsing() without exclusive locking
* if (q.readLock().tryLock()) {
* MapEntry<K, V> entry = q.entry();
* if (entry != null) {
* // Entry is present, return
* returnValue.returnValue(entry.value());
* return;
* }
* // Key is absent
* // Need to unlock, to lock to update lock later. Direct upgrade is forbidden.
* q.readLock().unlock();
* }
* // We are here, either if we:
* // 1) Failed to acquire the read lock, this means some other thread is holding the write
* // lock now, in this case waiting for the update lock acquisition is no longer, than for
* // the read
* // 2) Seen the entry is absent under the read lock. This means we need to insert
* // the default value into the map. that requires update-level access as well
* q.updateLock().lock();
* MapEntry<K, V> entry = q.entry();
* if (entry != null) {
* // Entry is present, return
* returnValue.returnValue(entry.value());
* return;
* }
* // Key is absent
* q.insert(q.absentEntry(), q.defaultValue(q.absentEntry()));
* returnValue.returnValue(q.entry().value());
* }}</pre></li>
* <li>If the default {@link InterProcessLock#lock()} policy of trying to acquire the
* lock for some time, and then throw {@code RuntimeException}, or need a custom timeout:
* <pre>{@code
* try (ExternalHashQueryContext<K> q = hash.queryContext(key)) {
* if (q.writeLock().tryLock(5, TimeUnit.SECONDS)) {
* // do something
* } else {
* // do something else, maybe not throwing an exception
* }
* }}</pre></li>
* </ul>
*
* <p>{@code HashQueryContext} defines the common pattern for working with {@code ChronicleHash}
* contexts: it has a pair of methods, {@link #entry()} and {@link #absentEntry()}, at any moment
* one of them returns an (absent) entry context object, another - {@code null}, depending on the
* presence of the {@linkplain #queriedKey() queried key} in the {@code ChronicleHash}. Thus,
* block of code that uses {@code HashQueryContext} usually has an if-else statement,
* with "then" branch for dealing with the present entry, {@code else} branch for dealing with
* the absent entry, or vise-versa. For example: <pre>{@code
* interface Point {
* double getX();
* void setX(double x);
* double addX(double xAdd);
*
* double getY();
* void setY(double y);
* double addY(double yAdd);
* }
*
* <K> Point movePoint(ChronicleMap<K, Point> map, K key, double xMove, double yMove,
* Point using) {
* // Moves existing point by [xMove, yMove], if absent - assumes the default point is [0, 0].
* // Returns the resulting point
* try (ExternalMapQueryContext<K, Point, ?> q = map.queryContext(key)) {
* Point offHeapPoint;
* q.updateLock().lock();
* MapEntry<K, Point> entry = q.entry();
* if (entry != null) {
* // Key is present
* offHeapPoint = entry.value().getUsing(using);
* } else {
* // Key is absent
* q.insert(q.absentEntry(), q.defaultValue(q.absentEntry()));
* offHeapPoint = q.entry().value().getUsing(using);
* }
* offHeapPoint.addX(xMove);
* offHeapPoint.addY(yMove);
* return offHeapPoint;
* }
* }}</pre>
* <p>
* {@code HashQueryContext} is the base interface defining the structure, but it has no methods
* to anything "interesting" with {@code entry()} or {@code absentEntry()}. Use {@link
* MapQueryContext} or {@link SetQueryContext} interfaces, which provide access to {@link
* MapEntryOperations} and {@link SetEntryOperations} respectively.
*
* @param <K> the hash key type
* @see ChronicleHash#queryContext(Object)
*/
public interface HashQueryContext<K> extends HashContext<K>, SegmentLock {
/**
* Returns the index of the accessed segment, where the queried key is located (or to which
* the key is going to be put).
* <p>
* This index might also be used as the {@code InterProcessReadWriteUpdateLock} identifier,
* because {@code ChronicleHashes} has per-segment locks.
*/
@Override
int segmentIndex();
/**
* Returns the queried key as a {@code Data}.
*/
Data<K> queriedKey();
/**
* Returns the entry context, if the entry with the queried key is <i>present</i>
* in the {@code ChronicleHash}, returns {@code null} is the entry is <i>absent</i>.
* Might acquire {@link #readLock} before searching for the key, if the context
* is not locked yet.
*/
HashEntry<K> entry();
/**
* Returns the special <i>absent entry</i> object, if the entry with the queried key
* is <i>absent</i> in the hash, returns {@code null}, if the entry is <i>present</i>.
* Might acquire {@link #readLock} before searching for the key, if the context
* is not locked yet.
*/
@Nullable
HashAbsentEntry<K> absentEntry();
}