• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

evolvedbinary / elemental / 982

29 Apr 2025 08:34PM UTC coverage: 56.409% (+0.007%) from 56.402%
982

push

circleci

adamretter
[feature] Improve README.md badges

28451 of 55847 branches covered (50.94%)

Branch coverage included in aggregate %.

77468 of 131924 relevant lines covered (58.72%)

0.59 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

53.02
/exist-core/src/main/java/org/exist/storage/lock/LockTable.java
1
/*
2
 * Elemental
3
 * Copyright (C) 2024, Evolved Binary Ltd
4
 *
5
 * admin@evolvedbinary.com
6
 * https://www.evolvedbinary.com | https://www.elemental.xyz
7
 *
8
 * Use of this software is governed by the Business Source License 1.1
9
 * included in the LICENSE file and at www.mariadb.com/bsl11.
10
 *
11
 * Change Date: 2028-04-27
12
 *
13
 * On the date above, in accordance with the Business Source License, use
14
 * of this software will be governed by the Apache License, Version 2.0.
15
 *
16
 * Additional Use Grant: Production use of the Licensed Work for a permitted
17
 * purpose. A Permitted Purpose is any purpose other than a Competing Use.
18
 * A Competing Use means making the Software available to others in a commercial
19
 * product or service that: substitutes for the Software; substitutes for any
20
 * other product or service we offer using the Software that exists as of the
21
 * date we make the Software available; or offers the same or substantially
22
 * similar functionality as the Software.
23
 */
24
package org.exist.storage.lock;
25

26
import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet;
27
import net.jcip.annotations.ThreadSafe;
28
import org.apache.logging.log4j.Level;
29
import org.apache.logging.log4j.LogManager;
30
import org.apache.logging.log4j.Logger;
31
import org.exist.storage.NativeBroker;
32
import org.exist.storage.lock.Lock.LockMode;
33
import org.exist.storage.lock.Lock.LockType;
34
import org.exist.storage.txn.Txn;
35
import org.exist.util.Configuration;
36

37
import javax.annotation.Nullable;
38
import javax.annotation.concurrent.GuardedBy;
39
import java.util.*;
40
import java.util.concurrent.*;
41
import java.util.concurrent.locks.StampedLock;
42
import java.util.function.Consumer;
43

44
import static org.exist.storage.lock.LockTable.LockEventType.*;
45

46
/**
47
 * The Lock Table holds the details of
48
 * threads awaiting to acquire a Lock
49
 * and threads that have acquired a lock.
50
 *
51
 * It is arranged by the id of the lock
52
 * which is typically an indicator of the
53
 * lock subject.
54
 *
55
 * @author <a href="mailto:adam@evolvedbinary.com">Adam Retter</a>
56
 */
57
public class LockTable {
58

59
    // org.exist.util.Configuration properties
60
    public static final String CONFIGURATION_DISABLED = "lock-table.disabled";
61
    public static final String CONFIGURATION_TRACE_STACK_DEPTH = "lock-table.trace-stack-depth";
62

63
    //TODO(AR) remove eventually!
64
    // legacy properties for overriding the config
65
    public static final String PROP_DISABLE = "exist.locktable.disable";
66
    public static final String PROP_TRACE_STACK_DEPTH = "exist.locktable.trace.stack.depth";
67

68
    private static final Logger LOG = LogManager.getLogger(LockTable.class);
1✔
69
    private static final String THIS_CLASS_NAME = LockTable.class.getName();
1✔
70

71
    /**
72
     * Set to false to disable all events
73
     */
74
    private final boolean disableEvents;
75

76
    /**
77
     * Whether we should try and trace the stack for the lock event, -1 means all stack,
78
     * 0 means no stack, n means n stack frames, 5 is a reasonable value
79
     */
80
    private int traceStackDepth;
81

82
    /**
83
     * Lock event listeners
84
     */
85
    private final StampedLock listenersLock = new StampedLock();
1✔
86
    @GuardedBy("listenersWriteLock") private volatile LockEventListener[] listeners = null;
1✔
87

88
    /**
89
     * Table of threads attempting to acquire a lock
90
     */
91
    private final Map<Thread, Entry> attempting = new ConcurrentHashMap<>(60);
1✔
92

93
    /**
94
     * Table of threads which have acquired lock(s)
95
     */
96
    private final Map<Thread, Entries> acquired = new ConcurrentHashMap<>(60);
1✔
97

98

99
    LockTable(final Configuration configuration) {
1✔
100
        this.disableEvents = LockManager.getLegacySystemPropertyOrConfigPropertyBool(PROP_DISABLE, configuration, CONFIGURATION_DISABLED, false);
1✔
101
        this.traceStackDepth = LockManager.getLegacySystemPropertyOrConfigPropertyInt(PROP_TRACE_STACK_DEPTH, configuration, CONFIGURATION_TRACE_STACK_DEPTH, 0);
1✔
102

103
        // add a log listener if trace level logging is enabled
104
        if(LOG.isTraceEnabled()) {
1!
105
            registerListener(new LockEventLogListener(LOG, Level.TRACE));
×
106
        }
107
    }
1✔
108

109
    /**
110
     * Shuts down the lock table processor.
111
     *
112
     * After calling this, no further lock
113
     * events will be reported.
114
     */
115
    public void shutdown() {
116
    }
1✔
117

118
    /**
119
     * Set the depth at which we should trace lock events through the stack
120
     *
121
     * @param traceStackDepth -1 traces the whole stack, 0 means no stack traces, n means n stack frames
122
     */
123
    public void setTraceStackDepth(final int traceStackDepth) {
124
        this.traceStackDepth = traceStackDepth;
1✔
125
    }
1✔
126

127
    public void attempt(final long groupId, final String id, final LockType lockType, final LockMode mode) {
128
        event(Attempt, groupId, id, lockType, mode);
1✔
129
    }
1✔
130

131
    public void attemptFailed(final long groupId, final String id, final LockType lockType, final LockMode mode) {
132
        event(AttemptFailed, groupId, id, lockType, mode);
×
133
    }
×
134

135
    public void acquired(final long groupId, final String id, final LockType lockType, final LockMode mode) {
136
        event(Acquired, groupId, id, lockType, mode);
1✔
137
    }
1✔
138

139
    public void released(final long groupId, final String id, final LockType lockType, final LockMode mode) {
140
        event(Released, groupId, id, lockType, mode);
1✔
141
    }
1✔
142

143
    private void event(final LockEventType lockEventType, final long groupId, final String id, final LockType lockType, final LockMode lockMode) {
144
        if(disableEvents) {
1!
145
            return;
×
146
        }
147

148
        final long timestamp = System.nanoTime();
1✔
149
        final Thread currentThread = Thread.currentThread();
1✔
150

151
//        if(ignoreEvent(threadName, id)) {
152
//            return;
153
//        }
154

155
//        /**
156
//         * Very useful for debugging Lock life cycles
157
//         */
158
//        if (sanityCheck) {
159
//            sanityCheckLockLifecycles(lockEventType, groupId, id, lockType, lockMode, threadName, 1, timestamp, stackTrace);
160
//        }
161

162
        switch (lockEventType) {
1!
163
            case Attempt:
164

165
                Entry entry = attempting.get(currentThread);
1✔
166
                if (entry == null) {
1✔
167
                    // happens once per thread!
168
                    entry = new Entry();
1✔
169
                    attempting.put(currentThread, entry);
1✔
170
                }
171

172
                entry.id = id;
1✔
173
                entry.lockType = lockType;
1✔
174
                entry.lockMode = lockMode;
1✔
175
                entry.owner = currentThread.getName();
1✔
176
                if(traceStackDepth == 0) {
1✔
177
                    entry.stackTraces = null;
1✔
178
                } else {
1✔
179
                    entry.stackTraces = new ArrayList<>();
1✔
180
                    entry.stackTraces.add(getStackTrace(currentThread));
1✔
181
                }
182
                // write count last to ensure reader-thread visibility of above fields
183
                entry.count = 1;
1✔
184

185
                notifyListeners(lockEventType, timestamp, groupId, entry);
1✔
186

187
                break;
1✔
188

189

190
            case AttemptFailed:
191
                final Entry attemptFailedEntry = attempting.get(currentThread);
×
192
                if (attemptFailedEntry == null || attemptFailedEntry.count == 0) {
×
193
                    LOG.error("No entry found when trying to remove failed `attempt` for: id={}, thread={}", id, currentThread.getName());
×
194
                    break;
×
195
                }
196

197
                // mark attempt as unused
198
                attemptFailedEntry.count = 0;
×
199

200
                notifyListeners(lockEventType, timestamp, groupId, attemptFailedEntry);
×
201

202
                break;
×
203

204

205
            case Acquired:
206
                final Entry attemptEntry = attempting.get(currentThread);
1✔
207
                if (attemptEntry == null || attemptEntry.count == 0) {
1!
208
                    LOG.error("No entry found when trying to remove `attempt` to promote to `acquired` for: id={}, thread={}", id, currentThread.getName());
×
209

210
                    break;
×
211
                }
212

213
                // we now either add or merge the `attemptEntry` with the `acquired` table
214
                Entries acquiredEntries = acquired.get(currentThread);
1✔
215

216
                if (acquiredEntries == null) {
1✔
217
                    final Entry acquiredEntry = new Entry(attemptEntry);
1✔
218

219
                    acquiredEntries = new Entries(acquiredEntry);
1✔
220
                    acquired.put(currentThread, acquiredEntries);
1✔
221

222
                    notifyListeners(lockEventType, timestamp, groupId, acquiredEntry);
1✔
223

224
                } else {
1✔
225

226
                    final Entry acquiredEntry = acquiredEntries.merge(attemptEntry);
1✔
227
                    notifyListeners(lockEventType, timestamp, groupId, acquiredEntry);
1✔
228
                }
229

230
                // mark attempt as unused
231
                attemptEntry.count = 0;
1✔
232

233
                break;
1✔
234

235

236
            case Released:
237
                final Entries entries = acquired.get(currentThread);
1✔
238
                if (entries == null) {
1!
239
                    LOG.error("No entries found when trying to `release` for: id={}, thread={}", id, currentThread.getName());
×
240
                    break;
×
241
                }
242

243
                final Entry releasedEntry = entries.unmerge(id, lockType, lockMode);
1✔
244
                if (releasedEntry == null) {
1!
245
                    LOG.error("Unable to unmerge entry for `release`: id={}, threadName={}", id, currentThread.getName());
×
246
                    break;
×
247
                }
248

249
                notifyListeners(lockEventType, timestamp, groupId, releasedEntry);
1✔
250

251
                break;
252
        }
253
    }
1✔
254

255
    /**
256
     * There is one Entries object for each writing-thread,
257
     * however it may be read from other threads which
258
     * is why it needs to be thread-safe.
259
     */
260
    @ThreadSafe
261
    private static class Entries {
262
        private final StampedLock entriesLock = new StampedLock();
1✔
263
        @GuardedBy("entriesLock") private final ObjectLinkedOpenHashSet<Entry> entries = new ObjectLinkedOpenHashSet<>();
1✔
264

265
        public Entries(final Entry entry) {
1✔
266
            entries.add(entry);
1✔
267
        }
1✔
268

269
        public Entry merge(final Entry attemptEntry) {
270
            // try optimistic read
271
            long optReadStamp = entriesLock.tryOptimisticRead();
1✔
272
            long readStamp = -1;
1✔
273
            long stamp = -1;
1✔
274
            try {
275

276
                Entry local = entries.get(attemptEntry);
1✔
277
                if (!entriesLock.validate(optReadStamp)) {
1!
278

279
                    // otherwise... pessimistic read
280
                    readStamp = entriesLock.readLock();
×
281
                    stamp = readStamp;
×
282
                    local = entries.get(attemptEntry);
×
283
                } else {
×
284
                    stamp = optReadStamp;
1✔
285
                }
286

287
                // if found, do the merge
288
                if (local != null) {
1✔
289
                    if (attemptEntry.stackTraces != null) {
1✔
290
                        local.stackTraces.addAll(attemptEntry.stackTraces);
1✔
291
                    }
292
                    local.count += attemptEntry.count;
1✔
293
                    return local;
1✔
294
                }
295

296
                // try to upgrade optimistic-read or read lock to write lock
297
                stamp = entriesLock.tryConvertToWriteLock(stamp);
1✔
298
                if (stamp == 0L) {
1!
299

300
                    // failed to upgrade
301
                    if (readStamp != -1) {
×
302
                        // release the read lock before taking the write lock
303
                        entriesLock.unlockRead(readStamp);
×
304
                    }
305

306
                    // take the write lock (blocking)
307
                    stamp = entriesLock.writeLock();
×
308

309
                    // we must refresh the `local` as it could have changed between releasing the readLock and obtaining the write lock
310
                    local = entries.get(attemptEntry);
×
311

312
                    // if found, do the merge
313
                    if (local != null) {
×
314
                        if (attemptEntry.stackTraces != null) {
×
315
                            local.stackTraces.addAll(attemptEntry.stackTraces);
×
316
                        }
317
                        local.count += attemptEntry.count;
×
318
                        return local;
×
319
                    }
320
                }
321

322
                // we have a write lock, add it
323
                final Entry acquiredEntry = new Entry(attemptEntry);
1✔
324
                entries.add(acquiredEntry);
1✔
325
                return acquiredEntry;
1✔
326
            } finally {
327
                // we don't need to unlock if it was just an optimistic read
328
                if (stamp != optReadStamp) {
1✔
329
                    entriesLock.unlock(stamp);
1✔
330
                }
331
            }
332
        }
333

334
        @Nullable
335
        public Entry unmerge(final String id, final LockType lockType, final LockMode lockMode) {
336
            final Entry key = new Entry(id, lockType, lockMode, null, null);
1✔
337

338
            // optimistic read
339
            long stamp = entriesLock.tryOptimisticRead();
1✔
340
            Entry local = entries.get(key);
1✔
341
            // if count is equal to 1 we can just remove from the list rather than decrementing
342
            if (local.count == 1) {
1✔
343
                final long writeStamp = entriesLock.tryConvertToWriteLock(stamp);
1✔
344
                if (writeStamp != 0L) {
1!
345
                    try {
346
                        entries.remove(local);
1✔
347
                        local.count--;
1✔
348
                        return local;
1✔
349
                    } finally {
350
                        entriesLock.unlockWrite(writeStamp);
1✔
351
                    }
352
                }
353
            } else {
354
                if (entriesLock.validate(stamp)) {
1!
355

356
                    // do the unmerge bit
357
                    if (local.stackTraces != null) {
1✔
358
                        local.stackTraces.remove(local.stackTraces.size() - 1);
1✔
359
                    }
360
                    local.count = local.count - 1;
1✔
361

362
                    //done
363
                    return local;
1✔
364
                }
365
            }
366

367

368
            // otherwise... pessimistic read
369
            boolean mustRemove;
370
            stamp = entriesLock.readLock();
×
371
            try {
372

373
                local = entries.get(key);
×
374

375
                // if count is equal to 1 we can just remove from the list rather than decrementing
376
                if (local.count == 1) {
×
377

378
                    final long writeStamp = entriesLock.tryConvertToWriteLock(stamp);
×
379
                    if (writeStamp != 0L) {
×
380
                        stamp = writeStamp;  // NOTE: this causes the write lock to be released in the finally further down
×
381
                        entries.remove(local);
×
382
                        local.count--;
×
383
                        return local;
×
384
                    }
385

386
                } else {
387
                    // do the unmerge bit
388
                    if (local.stackTraces != null) {
×
389
                        local.stackTraces.remove(local.stackTraces.size() - 1);
×
390
                    }
391
                    local.count = local.count - 1;
×
392

393
                    //done
394
                    return local;
×
395
                }
396

397
                mustRemove = true;
×
398
            } finally {
×
399
                entriesLock.unlock(stamp);
×
400
            }
401

402
            // unable to remove by tryConvertToWriteLock above, so directly acquire write lock
403
            if (mustRemove) {
×
404
                stamp = entriesLock.writeLock();
×
405
                try {
406
                    entries.remove(local);
×
407
                    local.count--;
×
408
                    return local;
×
409
                } finally {
410
                    entriesLock.unlockWrite(stamp);
×
411
                }
412
            }
413

414
            return null;
×
415
        }
416

417
        public void forEach(final Consumer<Entry> entryConsumer) {
418
            final long stamp = entriesLock.readLock();
1✔
419
            try {
420
                entries.forEach(entryConsumer);
1✔
421
            } finally {
1✔
422
                entriesLock.unlockRead(stamp);
1✔
423
            }
424
        }
1✔
425
    }
426

427
    @Nullable
428
    private StackTraceElement[] getStackTrace(final Thread thread) {
429
        final StackTraceElement[] stackTrace = thread.getStackTrace();
1✔
430
        final int lastStackTraceElementIdx = stackTrace.length - 1;
1✔
431

432
        final int from = findFirstExternalFrame(stackTrace);
1✔
433
        final int to;
434
        if (traceStackDepth == -1) {
1!
435
            to = lastStackTraceElementIdx;
×
436
        } else {
×
437
            final int calcTo = from + traceStackDepth;
1✔
438
            if (calcTo > lastStackTraceElementIdx) {
1!
439
                to = lastStackTraceElementIdx;
×
440
            } else {
×
441
                to = calcTo;
1✔
442
            }
443
        }
444

445
        return Arrays.copyOfRange(stackTrace, from, to);
1✔
446
    }
447

448
    private int findFirstExternalFrame(final StackTraceElement[] stackTrace) {
449
        // we start with i = 1 to avoid Thread#getStackTrace() frame
450
        for(int i = 1; i < stackTrace.length; i++) {
1!
451
            if(!THIS_CLASS_NAME.equals(stackTrace[i].getClassName())) {
1✔
452
                return i;
1✔
453
            }
454
        }
455
        return 0;
×
456
    }
457

458
    public void registerListener(final LockEventListener lockEventListener) {
459
        final long stamp = listenersLock.writeLock();
1✔
460
        try {
461
            // extend listeners by 1
462
            if (listeners == null) {
1!
463
                listeners = new LockEventListener[1];
1✔
464
                listeners[0] = lockEventListener;
1✔
465
            } else {
1✔
466
                final LockEventListener[] newListeners = new LockEventListener[listeners.length + 1];
×
467
                System.arraycopy(listeners, 0, newListeners, 0, listeners.length);
×
468
                newListeners[listeners.length] = lockEventListener;
×
469
                listeners = newListeners;
×
470
            }
471
        } finally {
×
472
            listenersLock.unlockWrite(stamp);
1✔
473
        }
474

475
        lockEventListener.registered();
1✔
476
    }
1✔
477

478
    public void deregisterListener(final LockEventListener lockEventListener) {
479
        final long stamp = listenersLock.writeLock();
1✔
480
        try {
481
            // reduce listeners by 1
482
            for (int i = listeners.length - 1; i > -1; i--) {
1!
483
                // intentionally compare by identity!
484
                if (listeners[i] == lockEventListener) {
1!
485

486
                    if (i == 0 && listeners.length == 1) {
1!
487
                        listeners = null;
1✔
488
                        break;
1✔
489
                    }
490

491
                    final LockEventListener[] newListeners = new LockEventListener[listeners.length - 1];
×
492
                    System.arraycopy(listeners, 0, newListeners, 0, i);
×
493
                    if (listeners.length != i) {
×
494
                        System.arraycopy(listeners, i + 1, newListeners, i, listeners.length - i - 1);
×
495
                    }
496
                    listeners = newListeners;
×
497

498
                    break;
×
499
                }
500
            }
501
        } finally {
×
502
            listenersLock.unlockWrite(stamp);
1✔
503
        }
504

505
        lockEventListener.unregistered();
1✔
506
    }
1✔
507

508
    /**
509
     * Get's a copy of the current lock attempt information
510
     *
511
     * @return lock attempt information
512
     */
513
    public Map<String, Map<LockType, List<LockModeOwner>>> getAttempting() {
514
        final Map<String, Map<LockType, List<LockModeOwner>>> result = new HashMap<>();
1✔
515

516
        for (Entry entry : attempting.values()) {
1✔
517
            // read count (volatile) first to ensure visibility
518
            final int localCount = entry.count;
1✔
519
            if (localCount == 0) {
1!
520
                // attempt entry object is marked as unused
521
                continue;
1✔
522
            }
523

524
            result.compute(entry.id, (_k, v) -> {
×
525
                if (v == null) {
×
526
                    v = new HashMap<>();
×
527
                }
528

529
                v.compute(entry.lockType, (_k1, v1) -> {
×
530
                    if (v1 == null) {
×
531
                        v1 = new ArrayList<>();
×
532
                    }
533
                    v1.add(new LockModeOwner(entry.lockMode, entry.owner, entry.stackTraces != null ? entry.stackTraces.get(0) : null));
×
534
                    return v1;
×
535
                });
536

537
                return v;
×
538
            });
539
        }
540

541
        return result;
1✔
542
    }
543

544
    /**
545
     * Get's a copy of the current acquired lock information
546
     *
547
     * @return acquired lock information
548
     */
549
    public Map<String, Map<LockType, Map<LockMode, Map<String, LockCountTraces>>>> getAcquired() {
550
        final Map<String, Map<LockType, Map<LockMode, Map<String, LockCountTraces>>>> result = new HashMap<>();
1✔
551

552
        for (Entries entries : acquired.values()) {
1✔
553
            entries.forEach(entry -> {
1✔
554

555
                // read count (volatile) first to ensure visibility
556
                final int localCount = entry.count;
×
557

558
                result.compute(entry.id, (_k, v) -> {
×
559
                    if (v == null) {
×
560
                        v = new EnumMap<>(LockType.class);
×
561
                    }
562

563
                    v.compute(entry.lockType, (_k1, v1) -> {
×
564
                        if (v1 == null) {
×
565
                            v1 = new EnumMap<>(LockMode.class);
×
566
                        }
567

568
                        v1.compute(entry.lockMode, (_k2, v2) -> {
×
569
                            if (v2 == null) {
×
570
                                v2 = new HashMap<>();
×
571
                            }
572

573
                            v2.compute(entry.owner, (_k3, v3) -> {
×
574
                                if (v3 == null) {
×
575
                                    v3 = new LockCountTraces(localCount, entry.stackTraces);
×
576
                                } else {
×
577
                                    v3.count += localCount;
×
578
                                    if (entry.stackTraces != null) {
×
579
                                        v3.traces.addAll(entry.stackTraces);
×
580
                                    }
581
                                }
582

583
                                return v3;
×
584
                            });
585

586
                            return v2;
×
587

588
                        });
589

590
                        return v1;
×
591
                    });
592

593
                    return v;
×
594
                });
595
            });
×
596
        }
597

598
        return result;
1✔
599
    }
600

601
    public static class LockModeOwner {
602
        final LockMode lockMode;
603
        final String ownerThread;
604
        @Nullable final StackTraceElement[] trace;
605

606
        public LockModeOwner(final LockMode lockMode, final String ownerThread, @Nullable final StackTraceElement[] trace) {
×
607
            this.lockMode = lockMode;
×
608
            this.ownerThread = ownerThread;
×
609
            this.trace = trace;
×
610
        }
×
611

612
        public LockMode getLockMode() {
613
            return lockMode;
×
614
        }
615

616
        public String getOwnerThread() {
617
            return ownerThread;
×
618
        }
619

620
        @Nullable public StackTraceElement[] getTrace() {
621
            return trace;
×
622
        }
623
    }
624

625
    public static class LockCountTraces {
626
        int count;
627
        @Nullable final List<StackTraceElement[]> traces;
628

629
        public LockCountTraces(final int count, @Nullable final List<StackTraceElement[]> traces) {
×
630
            this.count = count;
×
631
            this.traces = traces;
×
632
        }
×
633

634
        public int getCount() {
635
            return count;
×
636
        }
637

638
        @Nullable
639
        public List<StackTraceElement[]> getTraces() {
640
            return traces;
×
641
        }
642
    }
643

644
    private void notifyListeners(final LockEventType lockEventType, final long timestamp, final long groupId,
645
            final Entry entry) {
646
        if (listeners == null) {
1✔
647
            return;
1✔
648
        }
649

650
        final long stamp = listenersLock.readLock();
1✔
651
        try {
652
            for (LockEventListener listener : listeners) {
1✔
653
                try {
654
                    listener.accept(lockEventType, timestamp, groupId, entry);
1✔
655
                } catch (final Exception e) {
1✔
656
                    LOG.error("Listener '{}' error: ", listener.getClass().getName(), e);
×
657
                }
658
            }
659
        } finally {
1✔
660
            listenersLock.unlockRead(stamp);
1✔
661
        }
662
    }
1✔
663

664
    private static @Nullable <T> List<T> List(@Nullable final T item) {
665
        if (item == null) {
×
666
            return null;
×
667
        }
668

669
        final List<T> list = new ArrayList<>();
×
670
        list.add(item);
×
671
        return list;
×
672
    }
673

674
    public interface LockEventListener {
675
        default void registered() {}
×
676
        void accept(final LockEventType lockEventType, final long timestamp, final long groupId, final Entry entry);
677
        default void unregistered() {}
×
678
    }
679

680
    public enum LockEventType {
1✔
681
        Attempt,
1✔
682
        AttemptFailed,
1✔
683
        Acquired,
1✔
684
        Released
1✔
685
    }
686

687
    public static String formatString(final LockEventType lockEventType, final long groupId, final String id,
688
            final LockType lockType, final LockMode lockMode, final String threadName, final int count,
689
            final long timestamp, @Nullable final StackTraceElement[] stackTrace) {
690
        final StringBuilder builder = new StringBuilder()
×
691
                .append(lockEventType.name())
×
692
                .append(' ')
×
693
                .append(lockType.name());
×
694

695
        if(groupId > -1) {
×
696
            builder
×
697
                    .append("#")
×
698
                    .append(groupId);
×
699
        }
700

701
        builder.append('(')
×
702
                .append(lockMode.toString())
×
703
                .append(") of ")
×
704
                .append(id);
×
705

706
        if(stackTrace != null) {
×
707
            final String reason = getSimpleStackReason(stackTrace);
×
708
            if(reason != null) {
×
709
                builder
×
710
                        .append(" for #")
×
711
                        .append(reason);
×
712
            }
713
        }
714

715
        builder
×
716
                .append(" by ")
×
717
                .append(threadName)
×
718
                .append(" at ")
×
719
                .append(timestamp);
×
720

721
        if (lockEventType == Acquired || lockEventType == Released) {
×
722
            builder
×
723
                    .append(". count=")
×
724
                    .append(Integer.toString(count));
×
725
        }
726

727
        return builder.toString();
×
728
    }
729

730
    private static final String NATIVE_BROKER_CLASS_NAME = NativeBroker.class.getName();
1✔
731
    private static final String COLLECTION_STORE_CLASS_NAME = NativeBroker.class.getName();
1✔
732
    private static final String TXN_CLASS_NAME = Txn.class.getName();
1✔
733

734
    @Nullable
735
    public static String getSimpleStackReason(final StackTraceElement[] stackTrace) {
736
        for (final StackTraceElement stackTraceElement : stackTrace) {
1✔
737
            final String className = stackTraceElement.getClassName();
1✔
738

739
            if (className.equals(NATIVE_BROKER_CLASS_NAME) || className.equals(COLLECTION_STORE_CLASS_NAME) || className.equals(TXN_CLASS_NAME)) {
1!
740
                if (!(stackTraceElement.getMethodName().endsWith("LockCollection") || stackTraceElement.getMethodName().equals("lockCollectionCache"))) {
1!
741
                    return stackTraceElement.getMethodName() + '(' + stackTraceElement.getLineNumber() + ')';
1✔
742
                }
743
            }
744
        }
745

746
        return null;
1✔
747
    }
748

749
    /**
750
     * Represents an entry in the {@link #attempting} or {@link #acquired} lock table.
751
     *
752
     * All class members are only written from a single
753
     * thread.
754
     *
755
     * However, they may be read from the same writer thread or a different read-only thread.
756
     * The member `count` is written last by the writer thread
757
     * and read first by the read-only reader thread to ensure correct visibility
758
     * of the member values.
759
     */
760
    public static class Entry {
761
        String id;
762
        LockType lockType;
763
        LockMode lockMode;
764
        String owner;
765

766
        @Nullable List<StackTraceElement[]> stackTraces;
767

768
        /**
769
         * Intentionally marked volatile.
770
         * All variables visible before this point become available
771
         * to the reading thread.
772
         */
773
        volatile int count = 0;
1✔
774

775
        private Entry() {
1✔
776
        }
1✔
777

778
        private Entry(final String id, final LockType lockType, final LockMode lockMode, final String owner,
1✔
779
                @Nullable final StackTraceElement[] stackTrace) {
780
            this.id = id;
1✔
781
            this.lockType = lockType;
1✔
782
            this.lockMode = lockMode;
1✔
783
            this.owner = owner;
1✔
784
            if (stackTrace != null) {
1!
785
                this.stackTraces = new ArrayList<>();
×
786
                this.stackTraces.add(stackTrace);
×
787
            } else {
×
788
                this.stackTraces = null;
1✔
789
            }
790
            // write last to ensure reader visibility of above fields!
791
            this.count = 1;
1✔
792
        }
1✔
793

794
        private Entry(final Entry other) {
1✔
795
            this.id = other.id;
1✔
796
            this.lockType = other.lockType;
1✔
797
            this.lockMode = other.lockMode;
1✔
798
            this.owner = other.owner;
1✔
799
            if (other.stackTraces != null) {
1✔
800
                this.stackTraces = (ArrayList)((ArrayList)other.stackTraces).clone();
1✔
801
            } else {
1✔
802
                this.stackTraces = null;
1✔
803
            }
804
            // write last to ensure reader visibility of above fields!
805
            this.count = other.count;
1✔
806
        }
1✔
807

808
        public void setFrom(final Entry entry) {
809
            this.id = entry.id;
×
810
            this.lockType = entry.lockType;
×
811
            this.lockMode = entry.lockMode;
×
812
            this.owner = entry.owner;
×
813
            if (entry.stackTraces != null) {
×
814
                this.stackTraces = new ArrayList<>(entry.stackTraces);
×
815
            } else {
×
816
                this.stackTraces = null;
×
817
            }
818
            // write last to ensure reader visibility of above fields!
819
            this.count = entry.count;
×
820
        }
×
821

822
        @Override
823
        public boolean equals(final Object o) {
824
            if (this == o) return true;
1✔
825
            if (o == null || Entry.class != o.getClass()) return false;
1!
826
            Entry entry = (Entry) o;
1✔
827
            return lockType == entry.lockType &&
1✔
828
                    lockMode == entry.lockMode
1✔
829
                    && id.equals(entry.id);
1✔
830
        }
831

832
        @Override
833
        public int hashCode() {
834
            int result = id.hashCode();
1✔
835
            result = 31 * result + lockType.hashCode();
1✔
836
            result = 31 * result + lockMode.hashCode();
1✔
837
            return result;
1✔
838
        }
839

840
        public String getId() {
841
            return id;
1✔
842
        }
843

844
        public LockType getLockType() {
845
            return lockType;
1✔
846
        }
847

848
        public LockMode getLockMode() {
849
            return lockMode;
1✔
850
        }
851

852
        public String getOwner() {
853
            return owner;
1✔
854
        }
855

856
        @Nullable
857
        public List<StackTraceElement[]> getStackTraces() {
858
            return stackTraces;
1✔
859
        }
860

861
        public int getCount() {
862
            return count;
1✔
863
        }
864
    }
865

866

867
    /** debugging tools below **/
868

869
//    public static final String PROP_SANITY_CHECK = "exist.locktable.sanity.check";
870
//
871
//    /**
872
//     * Set to true to enable sanity checking of lock leases
873
//     */
874
//    private volatile boolean sanityCheck = Boolean.getBoolean(PROP_SANITY_CHECK);
875
//
876
//    /**
877
//     * Holds a count of READ and WRITE locks by {@link Entry#id}
878
//     * Only used for debugging,see {@link #sanityCheckLockLifecycles(LockEventType, long, String, LockType,
879
//     *     LockMode, String, int, long, StackTraceElement[])}.
880
//     */
881
//    @GuardedBy("this") private final Map<String, Tuple2<Long, Long>> lockCounts = new HashMap<>();
882
//
883
//    /**
884
//     * Checks that there are not more releases that there are acquires
885
//     */
886
//    private void sanityCheckLockLifecycles(final LockEventType lockEventType, final long groupId, final String id,
887
//            final LockType lockType, final LockMode lockMode, final String threadName, final int count,
888
//            final long timestamp, @Nullable final StackTraceElement[] stackTrace) {
889
//        synchronized(lockCounts) {
890
//            long read = 0;
891
//            long write = 0;
892
//
893
//            final Tuple2<Long, Long> lockCount = lockCounts.get(id);
894
//            if(lockCount != null) {
895
//                read = lockCount._1;
896
//                write = lockCount._2;
897
//            }
898
//
899
//            if(lockEventType == Acquired) {
900
//                if(lockMode == LockMode.READ_LOCK) {
901
//                    read++;
902
//                } else if(lockMode == LockMode.WRITE_LOCK) {
903
//                    write++;
904
//                }
905
//            } else if(lockEventType == Released) {
906
//                if(lockMode == LockMode.READ_LOCK) {
907
//                    if(read == 0) {
908
//                        LOG.error("Negative READ_LOCKs", new IllegalStateException());
909
//                    }
910
//                    read--;
911
//                } else if(lockMode == LockMode.WRITE_LOCK) {
912
//                    if(write == 0) {
913
//                        LOG.error("Negative WRITE_LOCKs", new IllegalStateException());
914
//                    }
915
//                    write--;
916
//                }
917
//            }
918
//
919
//            if(LOG.isTraceEnabled()) {
920
//                LOG.trace("QUEUE: {} (read={} write={})", formatString(lockEventType, groupId, id, lockType, lockMode,
921
//                        threadName, count, timestamp, stackTrace), read, write);
922
//            }
923
//
924
//            lockCounts.put(id, Tuple(read, write));
925
//        }
926
//    }
927

928
//    /**
929
//     * Simple filtering to ignore events that are not of interest
930
//     *
931
//     * @param threadName The name of the thread that triggered the event
932
//     * @param id The id of the lock
933
//     *
934
//     * @return true if the event should be ignored
935
//     */
936
//    private boolean ignoreEvent(final String threadName, final String id) {
937
//        // useful for debugging specific log events
938
//        return threadName.startsWith("DefaultQuartzScheduler_")
939
//                || id.equals("dom.dbx")
940
//                || id.equals("collections.dbx")
941
//                || id.equals("collections.dbx")
942
//                || id.equals("structure.dbx")
943
//                || id.equals("values.dbx")
944
//                || id.equals("CollectionCache");
945
//    }
946
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc