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

eXist-db / exist / 40510889971

27 Oct 2026 01:18PM UTC coverage: 58.196% (+0.02%) from 58.173%
40510889971

push

github

web-flow
Merge pull request #6934 from eXist-db/dependabot/maven/net.bytebuddy-byte-buddy-1.18.16

Bump net.bytebuddy:byte-buddy from 1.18.15 to 1.18.16

32506 of 61752 branches covered (52.64%)

Branch coverage included in aggregate %.

85629 of 141244 relevant lines covered (60.62%)

2.73 hits per line

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

64.71
/exist-core/src/main/java/org/exist/storage/BrokerPool.java
1
/*
2
 * eXist-db Open Source Native XML Database
3
 * Copyright (C) 2001 The eXist-db Authors
4
 *
5
 * info@exist-db.org
6
 * http://www.exist-db.org
7
 *
8
 * This library is free software; you can redistribute it and/or
9
 * modify it under the terms of the GNU Lesser General Public
10
 * License as published by the Free Software Foundation; either
11
 * version 2.1 of the License, or (at your option) any later version.
12
 *
13
 * This library is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16
 * Lesser General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU Lesser General Public
19
 * License along with this library; if not, write to the Free Software
20
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
21
 */
22
package org.exist.storage;
23

24
import com.evolvedbinary.j8fu.fsm.AtomicFSM;
25
import com.evolvedbinary.j8fu.fsm.FSM;
26
import com.evolvedbinary.j8fu.lazy.AtomicLazyVal;
27
import net.jcip.annotations.ThreadSafe;
28
import org.apache.logging.log4j.LogManager;
29
import org.apache.logging.log4j.Logger;
30
import org.exist.Database;
31
import org.exist.EXistException;
32
import org.exist.collections.Collection;
33
import org.exist.collections.CollectionCache;
34
import org.exist.collections.CollectionConfiguration;
35
import org.exist.collections.CollectionConfigurationManager;
36
import org.exist.collections.triggers.*;
37
import org.exist.config.ConfigurationDocumentTrigger;
38
import org.exist.config.Configurator;
39
import org.exist.config.annotation.ConfigurationClass;
40
import org.exist.config.annotation.ConfigurationFieldAsAttribute;
41
import org.exist.debuggee.Debuggee;
42
import org.exist.debuggee.DebuggeeFactory;
43
import org.exist.dom.persistent.SymbolTable;
44
import org.exist.indexing.IndexManager;
45
import org.exist.management.AgentFactory;
46
import org.exist.numbering.DLNFactory;
47
import org.exist.numbering.NodeIdFactory;
48
import org.exist.repo.ClasspathHelper;
49
import org.exist.repo.ExistRepository;
50
import org.exist.scheduler.Scheduler;
51
import org.exist.scheduler.impl.QuartzSchedulerImpl;
52
import org.exist.scheduler.impl.SystemTaskJobImpl;
53
import org.exist.security.SecurityManager;
54
import org.exist.security.*;
55
import org.exist.security.internal.SecurityManagerImpl;
56
import org.exist.storage.blob.BlobStore;
57
import org.exist.storage.blob.BlobStoreImplService;
58
import org.exist.storage.blob.BlobStoreService;
59
import org.exist.storage.vector.VectorStoreService;
60
import org.exist.storage.journal.JournalManager;
61
import org.exist.storage.lock.FileLockService;
62
import org.exist.storage.lock.LockManager;
63
import org.exist.storage.recovery.RecoveryManager;
64
import org.exist.storage.sync.Sync;
65
import org.exist.storage.sync.SyncTask;
66
import org.exist.storage.txn.TransactionException;
67
import org.exist.storage.txn.TransactionManager;
68
import org.exist.storage.txn.Txn;
69
import org.exist.util.*;
70
import org.exist.xmldb.ShutdownListener;
71
import org.exist.xmldb.XmldbURI;
72
import org.exist.xquery.PerformanceStats;
73
import org.exist.xquery.PerformanceStatsService;
74
import org.exist.xquery.XQuery;
75

76
import java.io.IOException;
77
import java.io.PrintWriter;
78
import java.io.StringWriter;
79
import java.lang.ref.Reference;
80
import java.lang.reflect.Array;
81
import java.lang.reflect.Field;
82
import java.nio.file.FileStore;
83
import java.nio.file.Path;
84
import java.text.NumberFormat;
85
import java.util.*;
86
import java.util.Map.Entry;
87
import java.util.concurrent.ConcurrentHashMap;
88
import java.util.concurrent.ConcurrentSkipListSet;
89
import java.util.concurrent.atomic.AtomicBoolean;
90
import java.util.concurrent.locks.Lock;
91
import java.util.concurrent.locks.ReentrantLock;
92
import java.util.function.Consumer;
93

94
import static com.evolvedbinary.j8fu.fsm.TransitionTable.transitionTable;
95
import static org.exist.util.ThreadUtils.nameInstanceThreadGroup;
96
import static org.exist.util.ThreadUtils.newInstanceDaemonThread;
97
import static org.exist.util.ThreadUtils.newInstanceThread;
98

99
/**
100
 * This class controls all available instances of the database.
101
 * Use it to configure, start and stop database instances.
102
 * You may have multiple instances defined, each using its own configuration.
103
 * To define multiple instances, pass an identification string to
104
 * {@link #configure(String, int, int, Configuration, Optional)}
105
 * and use {@link #getInstance(String)} to retrieve an instance.
106
 *
107
 * @author <a href="mailto:wolfgang@exist-db.org">Wolfgang Meier</a>
108
 * @author <a href="mailto:pierrick.brihaye@free.fr">Pierrick Brihaye</a>
109
 * @author <a href="mailto:adam@exist-db.org">Adam Retter</a>
110
 */
111
@ConfigurationClass("pool")
112
public class BrokerPool extends BrokerPools implements BrokerPoolConstants, Database {
113

114
    private final static Logger LOG = LogManager.getLogger(BrokerPool.class);
3 ✔
115

116
    private final BrokerPoolServicesManager servicesManager = new BrokerPoolServicesManager();
5 ✔
117

118
    private StatusReporter statusReporter = null;
3 ✔
119

120
    private final XQuery xqueryService = new XQuery();
5 ✔
121

122
    private AtomicLazyVal<SaxonConfiguration> saxonConfiguration = new AtomicLazyVal<>(() -> SaxonConfiguration.loadConfiguration(this));
10 ✔
123

124
    //TODO : make it non-static since every database instance may have its own policy.
125
    //TODO : make a default value that could be overwritten by the configuration
126
    // WM: this is only used by junit tests to test the recovery process.
127
    /**
128
     * For testing only: triggers a database corruption by disabling the page caches. The effect is
129
     * similar to a sudden power loss or the jvm being killed. The flag is used by some
130
     * junit tests to test the recovery process.
131
     */
132
    public static boolean FORCE_CORRUPTION = false;
3 ✔
133

134
    /**
135
     * <code>true</code> if the database instance is able to perform recovery.
136
     */
137
    private final boolean recoveryEnabled;
138

139
    /**
140
     * The name of the database instance
141
     */
142
    private final String instanceName;
143

144
    private final int concurrencyLevel;
145
    private LockManager lockManager;
146

147
    /**
148
     * Root thread group for all threads related
149
     * to this instance.
150
     */
151
    private final ThreadGroup instanceThreadGroup;
152

153
    /**
154
     * State of the BrokerPool instance
155
     */
156
    private enum State {
32 ✔
157
        SHUTTING_DOWN_MULTI_USER_MODE,
6 ✔
158
        SHUTTING_DOWN_SYSTEM_MODE,
6 ✔
159
        SHUTDOWN,
6 ✔
160
        INITIALIZING,
6 ✔
161
        INITIALIZING_SYSTEM_MODE,
6 ✔
162
        INITIALIZING_MULTI_USER_MODE,
6 ✔
163
        OPERATIONAL
6 ✔
164
    }
165

166
    private enum Event {
32 ✔
167
        INITIALIZE,
6 ✔
168
        INITIALIZE_SYSTEM_MODE,
6 ✔
169
        INITIALIZE_MULTI_USER_MODE,
6 ✔
170
        READY,
6 ✔
171

172
        START_SHUTDOWN_MULTI_USER_MODE,
6 ✔
173
        START_SHUTDOWN_SYSTEM_MODE,
6 ✔
174
        FINISHED_SHUTDOWN,
6 ✔
175
    }
176

177
    @SuppressWarnings("unchecked")
178
    private final FSM<State, Event> status = new AtomicFSM<>(State.SHUTDOWN,
6 ✔
179
            transitionTable(State.class, Event.class)
3 ✔
180
                    .when(State.SHUTDOWN).on(Event.INITIALIZE).switchTo(State.INITIALIZING)
6 ✔
181
                    .when(State.INITIALIZING).on(Event.INITIALIZE_SYSTEM_MODE).switchTo(State.INITIALIZING_SYSTEM_MODE)
6 ✔
182
                    .when(State.INITIALIZING_SYSTEM_MODE).on(Event.INITIALIZE_MULTI_USER_MODE).switchTo(State.INITIALIZING_MULTI_USER_MODE)
6 ✔
183
                    .when(State.INITIALIZING_MULTI_USER_MODE).on(Event.READY).switchTo(State.OPERATIONAL)
6 ✔
184
                    .when(State.OPERATIONAL).on(Event.START_SHUTDOWN_MULTI_USER_MODE).switchTo(State.SHUTTING_DOWN_MULTI_USER_MODE)
6 ✔
185
                    .when(State.SHUTTING_DOWN_MULTI_USER_MODE).on(Event.START_SHUTDOWN_SYSTEM_MODE).switchTo(State.SHUTTING_DOWN_SYSTEM_MODE)
6 ✔
186
                    .when(State.SHUTTING_DOWN_SYSTEM_MODE).on(Event.FINISHED_SHUTDOWN).switchTo(State.SHUTDOWN)
6 ✔
187
            .build()
1 ✔
188
    );
189

190

191
    public String getStatus() {
192
        return status.getCurrentState().name();
6 ✔
193
    }
194

195
    /**
196
     * The number of brokers for the database instance
197
     */
198
    private int brokersCount = 0;
3 ✔
199

200
    /**
201
     * The minimal number of brokers for the database instance
202
     */
203
    @ConfigurationFieldAsAttribute("min")
204
    private final int minBrokers;
205

206
    /**
207
     * The maximal number of brokers for the database instance
208
     */
209
    @ConfigurationFieldAsAttribute("max")
210
    private final int maxBrokers;
211

212
    /**
213
     * The number of inactive brokers for the database instance
214
     */
215
    private final Deque<DBBroker> inactiveBrokers = new ArrayDeque<>();
5 ✔
216

217
    /**
218
     * The number of active brokers for the database instance
219
     */
220
    private final Map<Thread, DBBroker> activeBrokers = new ConcurrentHashMap<>();
5 ✔
221

222

223
    /**
224
     * Used when TRACE level logging is enabled
225
     * to provide a history of broker leases
226
     */
227
    private final Map<String, TraceableStateChanges<TraceableBrokerLeaseChange.BrokerInfo, TraceableBrokerLeaseChange.Change>> brokerLeaseChangeTrace = LOG.isTraceEnabled() ? new HashMap<>() : null;
6 !
228
    private final Map<String, List<TraceableStateChanges<TraceableBrokerLeaseChange.BrokerInfo, TraceableBrokerLeaseChange.Change>>> brokerLeaseChangeTraceHistory = LOG.isTraceEnabled() ? new HashMap<>() : null;
6 !
229

230
    /**
231
     * The configuration object for the database instance
232
     */
233
    private final Configuration conf;
234

235
    private final ConcurrentSkipListSet<Observer> statusObservers = new ConcurrentSkipListSet<>();
5 ✔
236

237
    /**
238
     * <code>true</code> if a cache synchronization event is scheduled
239
     */
240
    //TODO : rename as syncScheduled ?
241
    //TODO : alternatively, delete this member and create a Sync.NOSYNC event
242
    private boolean syncRequired = false;
3 ✔
243

244
    /**
245
     * The kind of scheduled cache synchronization event.
246
     * One of {@link org.exist.storage.sync.Sync}
247
     */
248
    private Sync syncEvent = Sync.MINOR;
3 ✔
249

250
    private boolean checkpoint = false;
3 ✔
251

252
    /**
253
     * Indicates whether the database is operating in read-only mode
254
     */
255
    private final AtomicBoolean readOnly = new AtomicBoolean();
5 ✔
256

257
    @ConfigurationFieldAsAttribute("pageSize")
258
    private final int pageSize;
259

260
    private FileLockService dataLock;
261

262
    /**
263
     * The journal manager of the database instance.
264
     */
265
    private Optional<JournalManager> journalManager = Optional.empty();
3 ✔
266

267
    /**
268
     * The transaction manager of the database instance.
269
     */
270
    private TransactionManager transactionManager = null;
3 ✔
271

272
    /**
273
     * The Blob Store of the database instance.
274
     */
275
    private BlobStoreService blobStoreService;
276

277
    /**
278
     * The Vector Store of the database instance.
279
     */
280
    private VectorStoreService vectorStoreService;
281

282
    /**
283
     * Delay (in ms) for running jobs to return when the database instance shuts down.
284
     */
285
    @ConfigurationFieldAsAttribute("wait-before-shutdown")
286
    private final long maxShutdownWait;
287

288
    /**
289
     * The scheduler for the database instance.
290
     */
291
    @ConfigurationFieldAsAttribute("scheduler")
292
    private Scheduler scheduler;
293

294
    /**
295
     * Manages pluggable index structures.
296
     */
297
    private IndexManager indexManager;
298

299
    /**
300
     * Global symbol table used to encode element and attribute qnames.
301
     */
302
    private SymbolTable symbols;
303

304
    /**
305
     * Cache synchronization on the database instance.
306
     */
307
    @ConfigurationFieldAsAttribute("sync-period")
308
    private final long majorSyncPeriod;        //the period after which a major sync should occur
309
    private long lastMajorSync = System.currentTimeMillis();    //time the last major sync occurred
3 ✔
310

311
    private final long diskSpaceMin;
312

313
    /**
314
     * The listener that is notified when the database instance shuts down.
315
     */
316
    private ShutdownListener shutdownListener = null;
3 ✔
317

318
    /**
319
     * The security manager of the database instance.
320
     */
321
    private SecurityManager securityManager = null;
3 ✔
322

323
    /**
324
     * The global notification service used to subscribe
325
     * to document updates.
326
     */
327
    private NotificationService notificationService = null;
3 ✔
328

329
    /**
330
     * The cache in which the database instance may store items.
331
     */
332

333
    private DefaultCacheManager cacheManager;
334

335
    private long reservedMem;
336

337
    /**
338
     * The pool in which the database instance's <strong>compiled</strong> XQueries are stored.
339
     */
340
    private XQueryPool xQueryPool;
341

342
    /**
343
     * The monitor in which the database instance's strong>running</strong> XQueries are managed.
344
     */
345
    private ProcessMonitor processMonitor;
346

347
    /**
348
     * Global performance stats to gather function execution statistics
349
     * from all queries running on this database instance.
350
     */
351
    private PerformanceStats xqueryStats;
352

353
    /**
354
     * The global manager for accessing collection configuration files from the database instance.
355
     */
356
    private CollectionConfigurationManager collectionConfigurationManager = null;
3 ✔
357

358
    /**
359
     * The cache in which the database instance's collections are stored.
360
     */
361
    //TODO : rename as collectionsCache ?
362
    private CollectionCache collectionCache;
363

364
    /**
365
     * The pool in which the database instance's readers are stored.
366
     */
367
    private XMLReaderPool xmlReaderPool;
368

369
    private final NodeIdFactory nodeFactory = new DLNFactory();
5 ✔
370

371
    private final Lock globalXUpdateLock = new ReentrantLock();
5 ✔
372

373
    private Subject serviceModeUser = null;
3 ✔
374
    // volatile: enterServiceMode/exitServiceMode write from the caller's thread;
375
    // isInServiceMode() can be called from any thread without holding the monitor.
376
    private volatile boolean inServiceMode = false;
3 ✔
377

378
    //the time that the database was started
379
    private final Calendar startupTime = Calendar.getInstance();
3 ✔
380

381
    private final Optional<BrokerWatchdog> watchdog;
382

383
    private final ClassLoader classLoader;
384

385
    private Optional<ExistRepository> expathRepo = Optional.empty();
3 ✔
386

387
    private StartupTriggersManager startupTriggersManager;
388

389
    /**
390
     * Creates and configures the database instance.
391
     *
392
     * @param instanceName A name for the database instance.
393
     * @param minBrokers   The minimum number of concurrent brokers for handling requests on the database instance.
394
     * @param maxBrokers   The maximum number of concurrent brokers for handling requests on the database instance.
395
     * @param conf         The configuration object for the database instance
396
     * @param statusObserver    Observes the status of this database instance
397
     *
398
     * @throws EXistException If the initialization fails.
399
     */
400
    //TODO : Then write a configure(int minBrokers, int maxBrokers, Configuration conf) method
401
    BrokerPool(final String instanceName, final int minBrokers, final int maxBrokers, final Configuration conf,
2 ✔
402
            final Optional<Observer> statusObserver) {
403

404
        final NumberFormat nf = NumberFormat.getNumberInstance();
2 ✔
405

406
        this.classLoader = Thread.currentThread().getContextClassLoader();
4 ✔
407
        this.instanceName = instanceName;
3 ✔
408
        this.instanceThreadGroup = new ThreadGroup(nameInstanceThreadGroup(instanceName));
7 ✔
409

410
        this.maxShutdownWait = conf.getProperty(BrokerPool.PROPERTY_SHUTDOWN_DELAY, DEFAULT_MAX_SHUTDOWN_WAIT);
9 ✔
411
        LOG.info("database instance '{}' will wait  {} ms during shutdown", instanceName, nf.format(this.maxShutdownWait));
8 ✔
412

413
        this.recoveryEnabled = conf.getProperty(PROPERTY_RECOVERY_ENABLED, true);
9 ✔
414
        LOG.info("database instance '{}' is enabled for recovery : {}", instanceName, this.recoveryEnabled);
7 ✔
415

416
        this.minBrokers = conf.getProperty(PROPERTY_MIN_CONNECTIONS, minBrokers);
9 ✔
417
        this.maxBrokers = conf.getProperty(PROPERTY_MAX_CONNECTIONS, maxBrokers);
9 ✔
418
        LOG.info("database instance '{}' will have between {} and {} brokers", instanceName, nf.format(this.minBrokers), nf.format(this.maxBrokers));
14 ✔
419

420
        this.majorSyncPeriod = conf.getProperty(PROPERTY_SYNC_PERIOD, DEFAULT_SYNCH_PERIOD);
9 ✔
421
        LOG.info("database instance '{}' will be synchronized every {} ms", instanceName, nf.format(/*this.*/majorSyncPeriod));
8 ✔
422

423
        // convert from bytes to megabytes: 1024 * 1024
424
        this.diskSpaceMin = 1024L * 1024L * conf.getProperty(BrokerPool.DISK_SPACE_MIN_PROPERTY, DEFAULT_DISK_SPACE_MIN);
12 ✔
425

426
        this.pageSize = conf.getProperty(PROPERTY_PAGE_SIZE, DEFAULT_PAGE_SIZE);
9 ✔
427

428
        //Configuration is valid, save it
429
        this.conf = conf;
3 ✔
430

431
        this.concurrencyLevel = Math.max(maxBrokers, 2 * Runtime.getRuntime().availableProcessors());
8 ✔
432

433
        statusObserver.ifPresent(this.statusObservers::add);
8 ✔
434

435
        this.watchdog = Optional.ofNullable(System.getProperty(BrokerWatchdog.TRACE_BROKERS_PROPERTY_NAME))
5 ✔
436
                .filter(value -> "yes".equals(value))
2 ✔
437
                .map(value -> new BrokerWatchdog());
2 ✔
438
    }
1 ✔
439

440
    /**
441
     * Initializes the database instance.
442
     *
443
     * @throws EXistException
444
     * @throws DatabaseConfigurationException
445
     */
446
    void initialize() throws EXistException, DatabaseConfigurationException {
447
        try {
448
            _initialize();
2 ✔
449
        } catch(final Throwable e) {
1 ✔
450
            // remove that file lock we may have acquired in canReadDataDir
451

452
            if (dataLock != null && !readOnly.get()) {
×
453
                dataLock.release();
×
454
            }
455

456
            switch (e) {
×
457
                case EXistException existException -> throw existException;
×
458
                case DatabaseConfigurationException databaseConfigurationException -> throw databaseConfigurationException;
×
459
                case null, default -> throw new EXistException(e);
×
460
            }
461
        }
462
    }
1 ✔
463

464
    private void _initialize() throws EXistException, DatabaseConfigurationException {
465
        this.lockManager = new LockManager(conf, concurrencyLevel);
9 ✔
466

467
        //Flag to indicate that we are initializing
468
        status.process(Event.INITIALIZE);
5 ✔
469

470
        if(LOG.isDebugEnabled()) {
3 !
471
            LOG.debug("initializing database instance '{}'...", instanceName);
×
472
        }
473

474
        // register core broker pool services
475
        this.scheduler = servicesManager.register(new QuartzSchedulerImpl(this));
10 ✔
476

477
        // NOTE: this must occur after the scheduler, and before any other service which requires access to the data directory
478
        this.dataLock = servicesManager.register(new FileLockService("dbx_dir.lck", BrokerPool.PROPERTY_DATA_DIR, NativeBroker.DEFAULT_DATA_DIR));
12 ✔
479

480
        this.securityManager = servicesManager.register(new SecurityManagerImpl(this));
10 ✔
481

482
        this.cacheManager = servicesManager.register(new DefaultCacheManager(this));
10 ✔
483
        this.xQueryPool = servicesManager.register(new XQueryPool());
9 ✔
484
        this.processMonitor = servicesManager.register(new ProcessMonitor());
9 ✔
485
        this.xqueryStats = servicesManager.register(new PerformanceStatsService());
9 ✔
486
        final XMLReaderObjectFactory xmlReaderObjectFactory = servicesManager.register(new XMLReaderObjectFactory());
8 ✔
487
        this.xmlReaderPool = servicesManager.register(new XMLReaderPool(xmlReaderObjectFactory, maxBrokers, 0));
13 ✔
488
        final int bufferSize = Optional.of(conf.getInteger(PROPERTY_COLLECTION_CACHE_SIZE))
7 ✔
489
                .filter(size -> size != -1)
8 !
490
                .orElse(DEFAULT_COLLECTION_BUFFER_SIZE);
5 ✔
491
        this.collectionCache = servicesManager.register(new CollectionCache());
9 ✔
492
        this.notificationService = servicesManager.register(new NotificationService());
9 ✔
493

494
        this.journalManager = recoveryEnabled ? Optional.of(new JournalManager()) : Optional.empty();
11 ✔
495
        journalManager.ifPresent(servicesManager::register);
9 ✔
496

497
        final SystemTaskManager systemTaskManager = servicesManager.register(new SystemTaskManager(this));
9 ✔
498
        this.transactionManager = servicesManager.register(new TransactionManager(this, journalManager, systemTaskManager));
13 ✔
499

500
        this.blobStoreService = servicesManager.register(new BlobStoreImplService());
9 ✔
501
        this.vectorStoreService = servicesManager.register(new org.exist.storage.vector.VectorStoreServiceImpl());
9 ✔
502

503
        this.symbols = servicesManager.register(new SymbolTable());
9 ✔
504

505
        this.expathRepo = Optional.of(new ExistRepository());
6 ✔
506
        expathRepo.ifPresent(servicesManager::register);
9 ✔
507
        servicesManager.register(new ClasspathHelper());
7 ✔
508

509
        this.indexManager = servicesManager.register(new IndexManager(this));
10 ✔
510

511
        //Get a manager to handle further collections configuration
512
        this.collectionConfigurationManager = servicesManager.register(new CollectionConfigurationManager(this));
10 ✔
513

514
        this.startupTriggersManager = servicesManager.register(new StartupTriggersManager());
9 ✔
515

516
        // this is just used for unit tests
517
        final BrokerPoolService testBrokerPoolService = (BrokerPoolService) conf.getProperty("exist.testBrokerPoolService");
6 ✔
518
        if (testBrokerPoolService != null) {
2 ✔
519
            servicesManager.register(testBrokerPoolService);
5 ✔
520
        }
521

522
        //configure the registered services
523
        try {
524
            servicesManager.configureServices(conf);
5 ✔
525
        } catch(final BrokerPoolServiceException e) {
1 ✔
526
            throw new EXistException(e);
×
527
        }
528

529
        // calculate how much memory is reserved for caches to grow
530
        final Runtime rt = Runtime.getRuntime();
2 ✔
531
        final long maxMem = rt.maxMemory();
3 ✔
532
        final long minFree = maxMem / 5;
4 ✔
533
        reservedMem = cacheManager.getTotalMem() + collectionCache.getMaxCacheSize() + minFree;
12 ✔
534
        LOG.debug("Reserved memory: {}; max: {}; min: {}", reservedMem, maxMem, minFree);
10 ✔
535

536
        //prepare the registered services, before entering system (single-user) mode
537
        try {
538
            servicesManager.prepareServices(this);
4 ✔
539
        } catch(final BrokerPoolServiceException e) {
1 ✔
540
            throw new EXistException(e);
×
541
        }
542

543
        //setup database synchronization job
544
        if(majorSyncPeriod > 0) {
5 !
545
            final SyncTask syncTask = new SyncTask();
4 ✔
546
            syncTask.configure(conf, null);
5 ✔
547
            scheduler.createPeriodicJob(2500, new SystemTaskJobImpl(SyncTask.getJobName(), syncTask), 2500);
11 ✔
548
        }
549

550
        try {
551
            statusReporter = new StatusReporter(SIGNAL_STARTUP);
6 ✔
552
            statusObservers.forEach(statusReporter::addObserver);
9 ✔
553

554
            final Thread statusThread = newInstanceDaemonThread(this, "startup-status-reporter", statusReporter);
6 ✔
555
            statusThread.start();
2 ✔
556

557
            // statusReporter may have to be terminated or the thread can/will hang.
558
            try {
559
                final boolean exportOnly = conf.getProperty(PROPERTY_EXPORT_ONLY, false);
9 ✔
560

561
                // If the initialization fails after transactionManager has been created this method better cleans up
562
                // or the FileSyncThread for the journal can/will hang.
563
                try {
564

565
                    // Enter System Mode
566
                    try(final DBBroker systemBroker = get(Optional.of(securityManager.getSystemSubject()))) {
11 ✔
567

568
                        status.process(Event.INITIALIZE_SYSTEM_MODE);
5 ✔
569

570
                        if(isReadOnly()) {
3 !
571
                            journalManager.ifPresent(JournalManager::disableJournalling);
×
572
                        }
573

574
                        try(final Txn transaction = transactionManager.beginTransaction()) {
8 ✔
575
                            servicesManager.startPreSystemServices(systemBroker, transaction);
5 ✔
576
                            transaction.commit();
2 ✔
577
                        } catch(final BrokerPoolServiceException e) {
×
578
                            throw new EXistException(e);
×
579
                        }
580

581
                        //Run the recovery process
582
                        boolean recovered = false;
2 ✔
583
                        if(isRecoveryEnabled()) {
3 ✔
584
                            recovered = runRecovery(systemBroker);
4 ✔
585
                            //TODO : extract the following from this block ? What if we are not transactional ? -pb
586
                            if(!recovered) {
2 ✔
587
                                try {
588
                                    if(systemBroker.getCollection(XmldbURI.ROOT_COLLECTION_URI) == null) {
4 ✔
589
                                        final Txn txn = transactionManager.beginTransaction();
4 ✔
590
                                        try {
591
                                            systemBroker.getOrCreateCollection(txn, XmldbURI.ROOT_COLLECTION_URI);
5 ✔
592
                                            transactionManager.commit(txn);
4 ✔
593
                                        } catch(final IOException | TriggerException | PermissionDeniedException e) {
1 ✔
594
                                            transactionManager.abort(txn);
×
595
                                        } finally {
596
                                            transactionManager.close(txn);
4 ✔
597
                                        }
598
                                    }
599
                                } catch(final PermissionDeniedException pde) {
×
600
                                    LOG.fatal(pde.getMessage(), pde);
×
601
                                }
602
                            }
603
                        }
604

605
                        /* initialise required collections if they don't exist yet */
606
                        if(!exportOnly) {
2 !
607
                            try {
608
                                initialiseSystemCollections(systemBroker);
3 ✔
609
                            } catch(final PermissionDeniedException pde) {
1 ✔
610
                                LOG.error(pde.getMessage(), pde);
×
611
                                throw new EXistException(pde.getMessage(), pde);
×
612
                            }
613
                        }
614

615
                        statusReporter.setStatus(SIGNAL_READINESS);
4 ✔
616

617
                        try(final Txn transaction = transactionManager.beginTransaction()) {
8 ✔
618
                            servicesManager.startSystemServices(systemBroker, transaction);
5 ✔
619
                            transaction.commit();
2 ✔
620
                        } catch(final BrokerPoolServiceException e) {
×
621
                            throw new EXistException(e);
×
622
                        }
623

624
                        //If necessary, launch a task to repair the DB
625
                        //TODO : merge this with the recovery process ?
626
                        if(isRecoveryEnabled() && recovered) {
5 ✔
627
                            if(!exportOnly) {
2 !
628
                                reportStatus("Reindexing database files...");
3 ✔
629
                                try {
630
                                    systemBroker.repair();
2 ✔
631
                                } catch(final PermissionDeniedException e) {
1 ✔
632
                                    LOG.warn("Error during recovery: {}", e.getMessage(), e);
×
633
                                }
634
                            }
635

636
                            if((Boolean) conf.getProperty(PROPERTY_RECOVERY_CHECK)) {
7 !
637
                                final ConsistencyCheckTask task = new ConsistencyCheckTask();
4 ✔
638
                                final Properties props = new Properties();
4 ✔
639
                                props.setProperty("backup", "no");
5 ✔
640
                                props.setProperty("output", "sanity");
5 ✔
641
                                task.configure(conf, props);
5 ✔
642
                                try (final Txn transaction = transactionManager.beginTransaction()) {
8 ✔
643
                                    task.execute(systemBroker, transaction);
4 ✔
644
                                    transaction.commit();
2 ✔
645
                                }
646
                            }
647
                        }
648

649
                        //OK : the DB is repaired; let's make a few RW operations
650
                        statusReporter.setStatus(SIGNAL_WRITABLE);
4 ✔
651

652
                        //initialize configurations watcher trigger
653
                        if(!exportOnly) {
2 !
654
                            try {
655
                                initialiseTriggersForCollections(systemBroker, XmldbURI.SYSTEM_COLLECTION_URI);
4 ✔
656
                            } catch(final PermissionDeniedException pde) {
1 ✔
657
                                //XXX: do not catch exception!
658
                                LOG.error(pde.getMessage(), pde);
×
659
                            }
660
                        }
661

662
                        // remove temporary docs
663
                        try {
664
                            systemBroker.cleanUpTempResources(true);
3 ✔
665
                        } catch(final PermissionDeniedException pde) {
1 ✔
666
                            LOG.error(pde.getMessage(), pde);
×
667
                        }
668

669
                        sync(systemBroker, Sync.MAJOR);
4 ✔
670

671
                        // we have completed all system mode operations
672
                        // we can now prepare those services which need
673
                        // system mode before entering multi-user mode
674
                        try(final Txn transaction = transactionManager.beginTransaction()) {
8 ✔
675
                            servicesManager.startPreMultiUserSystemServices(systemBroker, transaction);
5 ✔
676
                                                        transaction.commit();
2 ✔
677
                        } catch(final BrokerPoolServiceException e) {
×
678
                            throw new EXistException(e);
×
679
                        }
680
                    }
681

682
                    //Create a default configuration file for the root collection
683
                    //TODO : why can't we call this from within CollectionConfigurationManager ?
684
                    //TODO : understand why we get a test suite failure
685
                    //collectionConfigurationManager.checkRootCollectionConfigCollection(broker);
686
                    //collectionConfigurationManager.checkRootCollectionConfig(broker);
687

688
                    //Create the minimal number of brokers required by the configuration
689
                    for(int i = 1; i < minBrokers; i++) {
7 !
690
                        createBroker();
×
691
                    }
692

693
                    status.process(Event.INITIALIZE_MULTI_USER_MODE);
5 ✔
694

695
                    // register some MBeans to provide access to this instance
696
                    AgentFactory.getInstance().initDBInstance(this);
3 ✔
697

698
                    if(LOG.isDebugEnabled()) {
3 !
699
                        LOG.debug("database instance '{}' initialized", instanceName);
×
700
                    }
701

702
                    servicesManager.startMultiUserServices(this);
4 ✔
703

704
                    status.process(Event.READY);
5 ✔
705

706
                    statusReporter.setStatus(SIGNAL_STARTED);
4 ✔
707
                } catch(final Throwable t) {
1 ✔
708
                    transactionManager.shutdown();
×
709
                    throw t;
×
710
                }
711
            } catch(final EXistException e) {
×
712
                throw e;
×
713
            } catch(final Throwable t) {
×
714
                throw new EXistException(t.getMessage(), t);
×
715
            }
716
        } finally {
717
            if(statusReporter != null) {
3 !
718
                statusReporter.terminate();
3 ✔
719
                statusReporter = null;
3 ✔
720
            }
721
        }
722
    }
1 ✔
723

724
    /**
725
     * Initialise system collections, if it doesn't exist yet
726
     *
727
     * @param sysBroker        The system broker from before the brokerpool is populated
728
     * @param sysCollectionUri XmldbURI of the collection to create
729
     * @param permissions      The permissions to set on the created collection
730
     */
731
    private void initialiseSystemCollection(final DBBroker sysBroker, final XmldbURI sysCollectionUri, final int permissions) throws EXistException, PermissionDeniedException {
732
        Collection collection = sysBroker.getCollection(sysCollectionUri);
4 ✔
733
        if(collection == null) {
2 ✔
734
            final TransactionManager transact = getTransactionManager();
3 ✔
735
            try(final Txn txn = transact.beginTransaction()) {
7 ✔
736
                collection = sysBroker.getOrCreateCollection(txn, sysCollectionUri);
5 ✔
737
                if(collection == null) {
2 !
738
                    throw new IOException("Could not create system collection: " + sysCollectionUri);
×
739
                }
740
                collection.setPermissions(sysBroker, permissions);
4 ✔
741
                sysBroker.saveCollection(txn, collection);
4 ✔
742

743
                transact.commit(txn);
3 ✔
744
            } catch(final Exception e) {
×
745
                final String msg = "Initialisation of system collections failed: " + e.getMessage();
×
746
                LOG.error(msg, e);
×
747
                throw new EXistException(msg, e);
×
748
            }
749
        }
750
    }
1 ✔
751

752
    /**
753
     * Initialize required system collections, if they don't exist yet
754
     *
755
     * @param broker - The system broker from before the brokerpool is populated
756
     * @throws EXistException If a system collection cannot be created
757
     */
758
    private void initialiseSystemCollections(final DBBroker broker) throws EXistException, PermissionDeniedException {
759
        //create /db/system
760
        initialiseSystemCollection(broker, XmldbURI.SYSTEM_COLLECTION_URI, Permission.DEFAULT_SYSTEM_COLLECTION_PERM);
5 ✔
761
    }
1 ✔
762

763
    private void initialiseTriggersForCollections(final DBBroker broker, final XmldbURI uri) throws EXistException, PermissionDeniedException {
764
        final Collection collection = broker.getCollection(uri);
4 ✔
765

766
        //initialize configurations watcher trigger
767
        if(collection != null) {
2 !
768
            final CollectionConfigurationManager manager = getConfigurationManager();
3 ✔
769
            final CollectionConfiguration collConf = manager.getOrCreateCollectionConfiguration(broker, collection);
5 ✔
770

771
            final DocumentTriggerProxy triggerProxy = new DocumentTriggerProxy(ConfigurationDocumentTrigger.class); //, collection.getURI());
5 ✔
772
            collConf.documentTriggers().add(triggerProxy);
5 ✔
773
        }
774
    }
1 ✔
775

776
    /**
777
     * Get the LockManager for this database instance
778
     *
779
     * @return The lock manager
780
     */
781
    public LockManager getLockManager() {
782
        return lockManager;
3 ✔
783
    }
784

785
    /**
786
     * Run a database recovery if required. This method is called once during
787
     * startup from {@link org.exist.storage.BrokerPool}.
788
     *
789
     * @param broker the database broker
790
     * @return true if recovery was run, false otherwise
791
     * @throws EXistException if a database error occurs
792
     */
793
    public boolean runRecovery(final DBBroker broker) throws EXistException {
794
        final boolean forceRestart = conf.getProperty(PROPERTY_RECOVERY_FORCE_RESTART, false);
9 ✔
795
        if(LOG.isDebugEnabled()) {
3 !
796
            LOG.debug("ForceRestart = {}", forceRestart);
×
797
        }
798
        if(journalManager.isPresent()) {
4 !
799
            final RecoveryManager recovery = new RecoveryManager(broker, journalManager.get(), forceRestart);
10 ✔
800
            return recovery.recover();
3 ✔
801
        } else {
802
            throw new IllegalStateException("Cannot run recovery without a JournalManager");
×
803
        }
804
    }
805

806
    public long getReservedMem() {
807
        return reservedMem - cacheManager.getCurrentSize();
7 ✔
808
    }
809

810
    public int getPageSize() {
811
        return pageSize;
3 ✔
812
    }
813

814
    /**
815
     * Returns the class loader used when this BrokerPool was configured.
816
     *
817
     * @return the classloader
818
     */
819
    public ClassLoader getClassLoader() {
820
        return this.classLoader;
3 ✔
821
    }
822

823
    /**
824
     * Whether or not the database instance is operational, i.e. initialization
825
     * has completed
826
     *
827
     * @return <code>true</code> if the database instance is operational
828
     */
829
    public boolean isOperational() {
830
        return status.getCurrentState() == State.OPERATIONAL;
×
831
    }
832

833
    /**
834
     * Returns the database instance's name.
835
     *
836
     * @return The id
837
     */
838
    //TODO : rename getInstanceName
839
    public String getId() {
840
        return instanceName;
3 ✔
841
    }
842

843
    @Override
844
    public ThreadGroup getThreadGroup() {
845
        return instanceThreadGroup;
3 ✔
846
    }
847

848
    /**
849
     * Returns the number of brokers currently serving requests for the database instance.
850
     *
851
     * @return The brokers count
852
     * @deprecated use countActiveBrokers
853
     */
854
    //TODO : rename as getActiveBrokers ?
855
    @Deprecated
856
    public int active() {
857
        return activeBrokers.size();
×
858
    }
859

860
    /**
861
     * Returns the number of brokers currently serving requests for the database instance.
862
     *
863
     * @return The active brokers count.
864
     */
865
    @Override
866
    public int countActiveBrokers() {
867
        return activeBrokers.size();
4 ✔
868
    }
869

870
    public Map<Thread, DBBroker> getActiveBrokers() {
871
        return new HashMap<>(activeBrokers);
6 ✔
872
    }
873

874
    /**
875
     * Returns the number of inactive brokers for the database instance.
876
     *
877
     * @return The brokers count
878
     */
879
    //TODO : rename as getInactiveBrokers ?
880
    public int available() {
881
        return inactiveBrokers.size();
4 ✔
882
    }
883

884
    //TODO : getMin() method ?
885

886
    /**
887
     * Returns the maximal number of brokers for the database instance.
888
     *
889
     * @return The brokers count
890
     */
891
    //TODO : rename as getMaxBrokers ?
892
    public int getMax() {
893
        return maxBrokers;
3 ✔
894
    }
895

896
    public int total() {
897
        return brokersCount;
3 ✔
898
    }
899

900
    /**
901
     * Returns whether the database instance has been configured.
902
     *
903
     * @return <code>true</code> if the datbase instance is configured
904
     */
905
    public final boolean isInstanceConfigured() {
906
        return conf != null;
5 !
907
    }
908

909
    /**
910
     * Returns the configuration object for the database instance.
911
     *
912
     * @return The configuration
913
     */
914
    public Configuration getConfiguration() {
915
        return conf;
3 ✔
916
    }
917

918
    public Optional<ExistRepository> getExpathRepo() {
919
        return expathRepo;
3 ✔
920
    }
921

922
    //TODO : rename as setShutdwonListener ?
923
    public void registerShutdownListener(final ShutdownListener listener) {
924
        //TODO : check that we are not shutting down
925
        shutdownListener = listener;
3 ✔
926
    }
1 ✔
927

928
    public NodeIdFactory getNodeFactory() {
929
        return nodeFactory;
3 ✔
930
    }
931

932
    /**
933
     * Returns the database instance's security manager
934
     *
935
     * @return The security manager
936
     */
937
    public SecurityManager getSecurityManager() {
938
        return securityManager;
3 ✔
939
    }
940

941

942
    /**
943
     * Returns the Scheduler
944
     *
945
     * @return The scheduler
946
     */
947
    public Scheduler getScheduler() {
948
        return scheduler;
3 ✔
949
    }
950

951
    @Override
952
    public BlobStore getBlobStore() {
953
        return blobStoreService.getBlobStore();
4 ✔
954
    }
955

956
    /**
957
     * Returns the Vector Store for persistent vector storage.
958
     *
959
     * @return the Vector Store, or null if not available
960
     */
961
    public org.exist.storage.vector.VectorStore getVectorStore() {
962
        return vectorStoreService != null ? vectorStoreService.getVectorStore() : null;
8 !
963
    }
964

965
    public SymbolTable getSymbols() {
966
        return symbols;
3 ✔
967
    }
968

969
    public NotificationService getNotificationService() {
970
        return notificationService;
3 ✔
971
    }
972

973
    /**
974
     * Returns whether transactions can be handled by the database instance.
975
     *
976
     * @return <code>true</code> if transactions can be handled
977
     */
978
    public boolean isRecoveryEnabled() {
979
        return !readOnly.get() && recoveryEnabled;
11 !
980
    }
981

982
    @Override
983
    public boolean isReadOnly() {
984
        final boolean isReadOnly = readOnly.get();
4 ✔
985
        if (!isReadOnly) {
2 !
986
            final long freeSpace = FileUtils.measureFileStore(dataLock.getFile(), FileStore::getUsableSpace);
6 ✔
987
            if (freeSpace != -1 && freeSpace < diskSpaceMin) {
9 !
988
                LOG.fatal("Partition containing DATA_DIR: {} is running out of disk space [minimum: {} free: {}]. Switching eXist-db into read-only mode to prevent data loss!", dataLock.getFile().toAbsolutePath().toString(), diskSpaceMin, freeSpace);
×
989
                setReadOnly();
×
990
            }
991
        }
992
        return readOnly.get();
4 ✔
993
    }
994

995
    public void setReadOnly() {
996
        if (readOnly.compareAndSet(false, true)) {
×
997
            LOG.warn("Switched database into read-only mode!");
×
998
        }
999
    }
×
1000

1001
    public boolean isInServiceMode() {
1002
        return inServiceMode;
3 ✔
1003
    }
1004

1005
    @Override
1006
    public Optional<JournalManager> getJournalManager() {
1007
        return journalManager;
3 ✔
1008
    }
1009

1010
    public TransactionManager getTransactionManager() {
1011
        return transactionManager;
3 ✔
1012
    }
1013

1014
    /**
1015
     * Returns a manager for accessing the database instance's collection configuration files.
1016
     *
1017
     * @return The manager
1018
     */
1019
    @Override
1020
    public CollectionConfigurationManager getConfigurationManager() {
1021
        return collectionConfigurationManager;
3 ✔
1022
    }
1023

1024
    /**
1025
     * Returns a cache in which the database instance's collections are stored.
1026
     *
1027
     * @return The cache
1028
     */
1029
    public CollectionCache getCollectionsCache() {
1030
        return collectionCache;
3 ✔
1031
    }
1032

1033
    /**
1034
     * Returns a cache in which the database instance's may store items.
1035
     *
1036
     * @return The cache
1037
     */
1038
    @Override
1039
    public DefaultCacheManager getCacheManager() {
1040
        return cacheManager;
3 ✔
1041
    }
1042

1043
    /**
1044
     * Returns the index manager which handles all additional indexes not
1045
     * being part of the database core.
1046
     *
1047
     * @return The IndexManager
1048
     */
1049
    @Override
1050
    public IndexManager getIndexManager() {
1051
        return indexManager;
3 ✔
1052
    }
1053

1054
    /**
1055
     * Returns a pool in which the database instance's <strong>compiled</strong> XQueries are stored.
1056
     *
1057
     * @return The pool
1058
     */
1059
    public XQueryPool getXQueryPool() {
1060
        return xQueryPool;
3 ✔
1061
    }
1062

1063
    /**
1064
     * Retuns the XQuery Service
1065
     *
1066
     * @return The XQuery service
1067
     */
1068
    public XQuery getXQueryService() {
1069
        return xqueryService;
3 ✔
1070
    }
1071

1072
    /**
1073
     * Returns a monitor in which the database instance's <strong>running</strong> XQueries are managed.
1074
     *
1075
     * @return The monitor
1076
     */
1077
    public ProcessMonitor getProcessMonitor() {
1078
        return processMonitor;
3 ✔
1079
    }
1080

1081
    /**
1082
     * Returns the global profiler used to gather execution statistics
1083
     * from all XQueries running on this db instance.
1084
     *
1085
     * @return the profiler
1086
     */
1087
    public PerformanceStats getPerformanceStats() {
1088
        return xqueryStats;
3 ✔
1089
    }
1090

1091
    /**
1092
     * Returns a pool in which the database instance's readers are stored.
1093
     *
1094
     * @return The pool
1095
     *
1096
     * @deprecated Use {@link #getXmlReaderPool()} instead
1097
     */
1098
    @Deprecated
1099
    public XMLReaderPool getParserPool() {
1100
        return xmlReaderPool;
3 ✔
1101
    }
1102

1103
    /**
1104
     * Returns a pool in which the database instance's readers are stored.
1105
     *
1106
     * @return The pool
1107
     */
1108
    public XMLReaderPool getXmlReaderPool() {
1109
        return xmlReaderPool;
3 ✔
1110
    }
1111

1112
    /**
1113
     * Returns the global update lock for the database instance.
1114
     * This lock is used by XUpdate operations to avoid that
1115
     * concurrent XUpdate requests modify the database until all
1116
     * document locks have been correctly set.
1117
     *
1118
     * @return The global lock
1119
     */
1120
    //TODO : rename as getUpdateLock ?
1121
    public Lock getGlobalUpdateLock() {
1122
        return globalXUpdateLock;
3 ✔
1123
    }
1124

1125
    /**
1126
     * Creates an inactive broker for the database instance.
1127
     *
1128
     * @return The broker
1129
     * @throws EXistException if the broker cannot be created
1130
     */
1131
    protected DBBroker createBroker() throws EXistException {
1132
        //TODO : in the future, don't pass the whole configuration, just the part relevant to brokers
1133
        final DBBroker broker = BrokerFactory.getInstance(this, this.getConfiguration());
5 ✔
1134
        inactiveBrokers.push(broker);
4 ✔
1135
        brokersCount++;
6 ✔
1136
        broker.setId(broker.getClass().getName() + '_' + instanceName + "_" + brokersCount);
10 ✔
1137
        if (LOG.isDebugEnabled()) {
3 !
1138
            LOG.debug("Created broker '{} for database instance '{}'", broker.getId(), instanceName);
×
1139
        }
1140
        return broker;
2 ✔
1141
    }
1142

1143
    /**
1144
     * Get active broker for current thread
1145
     *
1146
     * Note - If you call getActiveBroker() you must not call
1147
     * release on both the returned active broker and the original
1148
     * lease from {@link BrokerPool#getBroker()} or {@link BrokerPool#get(Optional)}
1149
     * otherwise release will have been called more than get!
1150
     *
1151
     * @return Database broker
1152
     * @throws RuntimeException NO broker available for current thread.
1153
     */
1154
    public DBBroker getActiveBroker() { //throws EXistException {
1155
        //synchronized(this) {
1156
        //Try to get an active broker
1157
        final DBBroker broker = activeBrokers.get(Thread.currentThread());
6 ✔
1158
        if(broker == null) {
2 !
1159
            final StringBuilder sb = new StringBuilder();
×
1160
            sb.append("Broker was not obtained for thread '");
×
1161
            sb.append(Thread.currentThread());
×
1162
            sb.append("'.");
×
1163
            sb.append(System.getProperty("line.separator"));
×
1164

1165
            for(final Entry<Thread, DBBroker> entry : activeBrokers.entrySet()) {
×
1166
                sb.append(entry.getKey());
×
1167
                sb.append(" = ");
×
1168
                sb.append(entry.getValue());
×
1169
                sb.append(System.getProperty("line.separator"));
×
1170
            }
1171

1172
            LOG.debug(sb.toString());
×
1173
            throw new RuntimeException(sb.toString());
×
1174
        }
1175
        return broker;
2 ✔
1176
        //}
1177
    }
1178

1179
    public DBBroker authenticate(final String username, final Object credentials) throws AuthenticationException {
1180
        final Subject subject = getSecurityManager().authenticate(username, credentials);
6 ✔
1181

1182
        try {
1183
            return get(Optional.ofNullable(subject));
5 ✔
1184
        } catch(final Exception e) {
×
1185
            throw new AuthenticationException(AuthenticationException.UNNOWN_EXCEPTION, e);
×
1186
        }
1187
    }
1188

1189
    /**
1190
     * Returns an active broker for the database instance.
1191
     *
1192
     * The current user will be inherited by this broker
1193
     *
1194
     * @return The broker
1195
     */
1196
    public DBBroker getBroker() throws EXistException {
1197
        return get(Optional.empty());
4 ✔
1198
    }
1199

1200
    /**
1201
     * Returns an active broker for the database instance.
1202
     *
1203
     * @param subject Optionally a subject to set on the broker, if a user is not provided then the
1204
     *                current user assigned to the broker will be re-used
1205
     * @return The broker
1206
     * @throws EXistException If the instance is not available (stopped or not configured)
1207
     */
1208
    //TODO : rename as getBroker ? getInstance (when refactored) ?
1209
    public DBBroker get(final Optional<Subject> subject) throws EXistException {
1210
        Objects.requireNonNull(subject, "Subject cannot be null, use BrokerPool#getBroker() instead");
4 ✔
1211

1212
        if(!isInstanceConfigured()) {
3 !
1213
            throw new EXistException("database instance '" + instanceName + "' is not available");
×
1214
        }
1215

1216
        //Try to get an active broker
1217
        DBBroker broker = activeBrokers.get(Thread.currentThread());
6 ✔
1218
        //Use it...
1219
        //TOUNDERSTAND (pb) : why not pop a broker from the inactive ones rather than maintaining reference counters ?
1220
        // WM: a thread may call this more than once in the sequence of operations, i.e. calls to get/release can
1221
        // be nested. Returning a new broker every time would lead to a deadlock condition if two threads have
1222
        // to wait for a broker to become available. We thus use reference counts and return
1223
        // the same broker instance for each thread.
1224
        if(broker != null) {
2 ✔
1225
            //increase its number of uses
1226
            broker.incReferenceCount();
2 ✔
1227
            broker.pushSubject(subject.orElseGet(broker::getCurrentSubject));
10 ✔
1228

1229
            if(LOG.isTraceEnabled()) {
3 !
1230
                if(!brokerLeaseChangeTrace.containsKey(broker.getId())) {
×
1231
                    brokerLeaseChangeTrace.put(broker.getId(), new TraceableStateChanges<>());
×
1232
                }
1233
                brokerLeaseChangeTrace.get(broker.getId()).add(TraceableBrokerLeaseChange.get(new TraceableBrokerLeaseChange.BrokerInfo(broker.getId(), broker.getReferenceCount())));
×
1234
            }
1235

1236
            return broker;
2 ✔
1237
            //TODO : share the code with what is below (including notifyAll) ?
1238
            // WM: notifyAll is not necessary if we don't have to wait for a broker.
1239
        }
1240

1241
        //No active broker : get one ASAP
1242

1243
        while(serviceModeUser != null && subject.isPresent() && !subject.equals(Optional.ofNullable(serviceModeUser))) {
3 !
1244
            if(isShuttingDown()) {
×
1245
                break;
×
1246
            }
1247
            try {
1248
                LOG.debug("Db instance is in service mode. Waiting for db to become available again ...");
×
1249
                wait(1000);
×
1250
            } catch(final InterruptedException e) {
×
1251
                Thread.currentThread().interrupt();
×
1252
                LOG.warn("Interrupted while waiting for service mode to end");
×
1253
                break;
×
1254
            }
1255
        }
1256

1257
        synchronized(this) {
4 ✔
1258
            //Are there any available brokers ?
1259
            if(inactiveBrokers.isEmpty()) {
4 ✔
1260
                if(isShuttingDownOrDown()) {
3 !
1261
                    throw new EXistException("BrokerPool is not operational (state: " + status.getCurrentState().name() + ")");
×
1262
                }
1263
                //There are no available brokers. If allowed...
1264
                if(brokersCount < maxBrokers)
5 ✔
1265
                //... create one
1266
                {
1267
                    createBroker();
3 ✔
1268
                } else
1 ✔
1269
                    //... or wait until there is one available
1270
                    while(inactiveBrokers.isEmpty()) {
4 ✔
1271
                        if(isShuttingDownOrDown()) {
3 !
1272
                            throw new EXistException("BrokerPool is shutting down");
×
1273
                        }
1274
                        LOG.debug("waiting for a broker to become available");
3 ✔
1275
                        try {
1276
                            this.wait(1000);
3 ✔
1277
                        } catch(final InterruptedException e) {
1 ✔
1278
                            Thread.currentThread().interrupt();
×
1279
                            throw new EXistException("Interrupted while waiting for a broker", e);
×
1280
                        }
1281
                    }
1282
            }
1283
            broker = inactiveBrokers.pop();
5 ✔
1284
            broker.prepare();
2 ✔
1285

1286
            //activate the broker
1287
            activeBrokers.put(Thread.currentThread(), broker);
6 ✔
1288

1289
            if(LOG.isTraceEnabled()) {
3 !
1290
                LOG.trace("+++ {}{}", Thread.currentThread(), Stacktrace.top(Thread.currentThread().getStackTrace(), Stacktrace.DEFAULT_STACK_TOP));
×
1291
            }
1292

1293
            if(watchdog.isPresent()) {
4 !
1294
                watchdog.get().add(broker);
×
1295
            }
1296

1297
            broker.incReferenceCount();
2 ✔
1298

1299
            broker.pushSubject(subject.orElseGet(securityManager::getGuestSubject));
11 ✔
1300

1301
            if(LOG.isTraceEnabled()) {
3 !
1302
                if(!brokerLeaseChangeTrace.containsKey(broker.getId())) {
×
1303
                    brokerLeaseChangeTrace.put(broker.getId(), new TraceableStateChanges<>());
×
1304
                }
1305
                brokerLeaseChangeTrace.get(broker.getId()).add(TraceableBrokerLeaseChange.get(new TraceableBrokerLeaseChange.BrokerInfo(broker.getId(), broker.getReferenceCount())));
×
1306
            }
1307

1308
            //Inform the other threads that we have a new-comer
1309
            // TODO: do they really need to be informed here???????
1310
            this.notifyAll();
2 ✔
1311
            return broker;
4 ✔
1312
        }
1313
    }
1314

1315
    /**
1316
     * Releases a broker for the database instance. If it is no more used, make if invactive.
1317
     * If there are pending system maintenance tasks,
1318
     * the method will block until these tasks have finished.
1319
     *
1320
     * NOTE - this is intentionally package-private, it is only meant to be
1321
     * called internally and from {@link DBBroker#close()}
1322
     *
1323
     * @param broker The broker to be released
1324
     */
1325
    //TODO : rename as releaseBroker ? releaseInstance (when refactored) ?
1326
    void release(final DBBroker broker) {
1327
        Objects.requireNonNull(broker, "Cannot release nothing");
4 ✔
1328

1329
        if(LOG.isTraceEnabled()) {
3 !
1330
            if(!brokerLeaseChangeTrace.containsKey(broker.getId())) {
×
1331
                brokerLeaseChangeTrace.put(broker.getId(), new TraceableStateChanges<>());
×
1332
            }
1333
            brokerLeaseChangeTrace.get(broker.getId()).add(TraceableBrokerLeaseChange.release(new TraceableBrokerLeaseChange.BrokerInfo(broker.getId(), broker.getReferenceCount())));
×
1334
        }
1335

1336
        //first check that the broker is active ! If not, return immediately.
1337
        broker.decReferenceCount();
2 ✔
1338
        if(broker.getReferenceCount() > 0) {
3 ✔
1339
            broker.popSubject();
3 ✔
1340
            //it is still in use and thus can't be marked as inactive
1341
            return;
1 ✔
1342
        }
1343

1344
        synchronized(this) {
7 ✔
1345
            //Broker is no more used : inactivate it
1346
            for(final DBBroker inactiveBroker : inactiveBrokers) {
12 ✔
1347
                if(broker == inactiveBroker) {
3 !
1348
                    LOG.error("Broker {} is already in the inactive list!!!", broker.getId());
×
1349
                    return;
×
1350
                }
1351
            }
1352

1353
            if(activeBrokers.remove(Thread.currentThread()) == null) {
5 !
1354
                LOG.error("release() has been called from the wrong thread for broker {}", broker.getId());
×
1355
                // Cleanup the state of activeBrokers
1356
                for(final Entry<Thread, DBBroker> activeBroker : activeBrokers.entrySet()) {
×
1357
                    if(activeBroker.getValue() == broker) {
×
1358
                        final String msg = "release() has been called from '" + Thread.currentThread() + "', but occupied at '" + activeBroker.getKey() + "'.";
×
1359
                        final EXistException ex = new EXistException(msg);
×
1360
                        LOG.error(msg, ex);
×
1361
                        activeBrokers.remove(activeBroker.getKey());
×
1362
                        break;
×
1363
                    }
1364
                }
1365
            } else {
×
1366
                if(LOG.isTraceEnabled()) {
3 !
1367
                    LOG.trace("--- {}{}", Thread.currentThread(), Stacktrace.top(Thread.currentThread().getStackTrace(), Stacktrace.DEFAULT_STACK_TOP));
×
1368
                }
1369
            }
1370
            
1371
            Subject lastUser = broker.popSubject();
3 ✔
1372

1373
            //guard to ensure that the broker has popped all its subjects
1374
            if(lastUser == null || broker.getCurrentSubject() != null) {
5 !
1375
                LOG.warn("Broker {} was returned with extraneous Subjects, cleaning...", broker.getId(), new IllegalStateException("DBBroker pushSubject/popSubject mismatch").fillInStackTrace());
×
1376
                if(LOG.isTraceEnabled()) {
×
1377
                    broker.traceSubjectChanges();
×
1378
                }
1379

1380
                //cleanup any remaining erroneous subjects
1381
                while(broker.getCurrentSubject() != null) {
×
1382
                    lastUser = broker.popSubject();
×
1383
                }
1384
            }
1385

1386
            inactiveBrokers.push(broker);
4 ✔
1387
            watchdog.ifPresent(wd -> wd.remove(broker));
5 ✔
1388

1389
            if(LOG.isTraceEnabled()) {
3 !
1390
                if(!brokerLeaseChangeTraceHistory.containsKey(broker.getId())) {
×
1391
                    brokerLeaseChangeTraceHistory.put(broker.getId(), new ArrayList<>());
×
1392
                }
1393
                try {
1394
                    brokerLeaseChangeTraceHistory.get(broker.getId()).add((TraceableStateChanges<TraceableBrokerLeaseChange.BrokerInfo, TraceableBrokerLeaseChange.Change>) brokerLeaseChangeTrace.get(broker.getId()).clone());
×
1395
                    brokerLeaseChangeTrace.get(broker.getId()).clear();
×
1396
                } catch(final CloneNotSupportedException e) {
×
1397
                    LOG.error(e);
×
1398
                }
1399

1400
                broker.clearSubjectChangesTrace();
×
1401
            }
1402

1403
            //If the database is now idle, do some useful stuff
1404
            if(activeBrokers.isEmpty()) {
4 ✔
1405
                //TODO : use a "clean" dedicated method (we have some below) ?
1406
                if(syncRequired) {
3 ✔
1407
                    //Note that the broker is not yet really inactive ;-)
1408
                    sync(broker, syncEvent);
5 ✔
1409
                    this.syncRequired = false;
3 ✔
1410
                    this.checkpoint = false;
3 ✔
1411
                }
1412
                if(serviceModeUser != null && !lastUser.equals(serviceModeUser)) {
3 !
1413
                    inServiceMode = true;
×
1414
                }
1415
            }
1416
            //Inform the other threads that someone is gone
1417
            this.notifyAll();
2 ✔
1418
        }
1419
    }
1 ✔
1420

1421
    public DBBroker enterServiceMode(final Subject user) throws PermissionDeniedException {
1422
        if(!user.hasDbaRole()) {
3 !
1423
            throw new PermissionDeniedException("Only users of group dba can switch the db to service mode");
×
1424
        }
1425

1426
        serviceModeUser = user;
3 ✔
1427
        synchronized(this) {
7 ✔
1428
            if(!activeBrokers.isEmpty()) {
4 !
1429
                while(!inServiceMode) {
×
1430
                    if(isShuttingDown()) {
×
1431
                        break;
×
1432
                    }
1433
                    try {
1434
                        wait(1000);
×
1435
                    } catch(final InterruptedException e) {
×
1436
                        Thread.currentThread().interrupt();
×
1437
                        break;
×
1438
                    }
1439
                }
1440
            }
1441
        }
1442

1443
        inServiceMode = true;
3 ✔
1444
        final DBBroker broker = inactiveBrokers.peek();
5 ✔
1445
        broker.prepare();
2 ✔
1446
        checkpoint = true;
3 ✔
1447
        sync(broker, Sync.MAJOR);
4 ✔
1448
        checkpoint = false;
3 ✔
1449
        // Return a broker that can be used to perform system tasks
1450
        return broker;
2 ✔
1451
    }
1452

1453
    public void exitServiceMode(final Subject user) throws PermissionDeniedException {
1454
        if(!user.equals(serviceModeUser)) {
5 !
1455
            throw new PermissionDeniedException("The db has been locked by a different user");
×
1456
        }
1457
        serviceModeUser = null;
3 ✔
1458
        inServiceMode = false;
3 ✔
1459
        synchronized(this) {
7 ✔
1460
            this.notifyAll();
2 ✔
1461
        }
1462
    }
1 ✔
1463

1464
    public void reportStatus(final String message) {
1465
        if(statusReporter != null) {
3 ✔
1466
            statusReporter.setStatus(message);
4 ✔
1467
        }
1468
    }
1 ✔
1469

1470
    public long getMajorSyncPeriod() {
1471
        return majorSyncPeriod;
3 ✔
1472
    }
1473

1474
    public long getLastMajorSync() {
1475
        return lastMajorSync;
3 ✔
1476
    }
1477

1478
    /**
1479
     * Executes a waiting cache synchronization for the database instance.
1480
     *
1481
     * NOTE: This method should not be called concurrently from multiple threads.
1482
     *
1483
     * @param broker    A broker responsible for executing the job
1484
     * @param syncEvent One of {@link org.exist.storage.sync.Sync}
1485
     */
1486
    public void sync(final DBBroker broker, final Sync syncEvent) {
1487

1488
        /**
1489
         * Database Systems - The Complete Book (Second edition)
1490
         * § 17.4.1 The Undo/Redo Rules
1491
         *
1492
         * The constraints that an undo/redo logging system must follow are summarized by the following rule:
1493
         *     * UR1  Before modifying any database element X on disk because of changes
1494
         *            made by some transaction T, it is necessary that the update record
1495
         *            <T,X,v,w> appear on disk.
1496
         */
1497
        journalManager.ifPresent(manager -> manager.flush(true, true));
9 ✔
1498

1499
        // sync various DBX files
1500
        broker.sync(syncEvent);
3 ✔
1501

1502
        //TODO : strange that it is set *after* the sunc method has been called.
1503
        try {
1504
            broker.pushSubject(securityManager.getSystemSubject());
5 ✔
1505

1506
            if (syncEvent == Sync.MAJOR) {
3 ✔
1507
                LOG.debug("Major sync");
3 ✔
1508
                try {
1509
                    if (!FORCE_CORRUPTION) {
2 ✔
1510
                        transactionManager.checkpoint(checkpoint);
5 ✔
1511
                    }
1512
                } catch (final TransactionException e) {
1 ✔
1513
                    LOG.warn(e.getMessage(), e);
×
1514
                }
1515
                cacheManager.checkCaches();
3 ✔
1516

1517
                lastMajorSync = System.currentTimeMillis();
3 ✔
1518
                if (LOG.isDebugEnabled()) {
3 !
1519
                    notificationService.debug();
×
1520
                }
1521
            } else {
×
1522
                cacheManager.checkDistribution();
3 ✔
1523
//            LOG.debug("Minor sync");
1524
            }
1525
            //TODO : touch this.syncEvent and syncRequired ?
1526
        } finally {
1 ✔
1527
            broker.popSubject();
3 ✔
1528
        }
1529
    }
1 ✔
1530

1531
    /**
1532
     * Schedules a system maintenance task for the database instance. If the database is idle,
1533
     * the task will be run immediately. Otherwise, the task will be deferred
1534
     * until all running threads have returned.
1535
     *
1536
     * @param task The task
1537
     */
1538
    //TOUNDERSTAND (pb) : synchronized, so... "schedules" or, rather, "executes" ?
1539
    public void triggerSystemTask(final SystemTask task) {
1540
        final State s = status.getCurrentState();
5 ✔
1541
        if(s == State.SHUTTING_DOWN_MULTI_USER_MODE || s == State.SHUTTING_DOWN_SYSTEM_MODE) {
6 !
1542
            LOG.info("Skipping SystemTask: '{}' as database is shutting down...", task.getName());
5 ✔
1543
            return;
1 ✔
1544
        } else if(s == State.SHUTDOWN) {
3 !
1545
            LOG.warn("Unable to execute SystemTask: '{}' as database is shut down!", task.getName());
×
1546
            return;
×
1547
        }
1548

1549
        transactionManager.triggerSystemTask(task);
4 ✔
1550
    }
1 ✔
1551

1552
    /**
1553
     * Shuts downs the database instance
1554
     */
1555
    public void shutdown() {
1556
        shutdown(false);
3 ✔
1557
    }
1 ✔
1558

1559
    /**
1560
     * Returns true if the BrokerPool is in the
1561
     * process of shutting down.
1562
     *
1563
     * @return true if the BrokerPool is shutting down.
1564
     */
1565
    public boolean isShuttingDown() {
1566
        final State s = status.getCurrentState();
5 ✔
1567
        return s == State.SHUTTING_DOWN_MULTI_USER_MODE
5 !
1568
                || s == State.SHUTTING_DOWN_SYSTEM_MODE;
3 !
1569
    }
1570

1571
    /**
1572
     * Returns true if the BrokerPool is either in the
1573
     * process of shutting down, or has already shutdown.
1574
     *
1575
     * @return true if the BrokerPool is shutting down or
1576
     *     has shutdown.
1577
     */
1578
    public boolean isShuttingDownOrDown() {
1579
        final State s = status.getCurrentState();
5 ✔
1580
        return s == State.SHUTTING_DOWN_MULTI_USER_MODE
7 !
1581
                || s == State.SHUTTING_DOWN_SYSTEM_MODE
3 ✔
1582
                || s == State.SHUTDOWN;
3 !
1583
    }
1584

1585
    /**
1586
     * Returns true of the BrokerPool is shutdown.
1587
     *
1588
     * @return true if the BrokerPool is shutdown.
1589
     */
1590
    public boolean isShutDown() {
1591
        return status.getCurrentState() == State.SHUTDOWN;
7 !
1592
    }
1593

1594
    /**
1595
     * Shuts downs the database instance
1596
     *
1597
     * @param killed <code>true</code> when the JVM is (cleanly) exiting
1598
     */
1599
    public void shutdown(final boolean killed) {
1600
        shutdown(killed, BrokerPools::removeInstance);
4 ✔
1601
    }
1 ✔
1602

1603
    void shutdown(final boolean killed, final Consumer<String> shutdownInstanceConsumer) {
1604

1605
        try {
1606
            status.process(Event.START_SHUTDOWN_MULTI_USER_MODE);
5 ✔
1607
        } catch(final IllegalStateException e) {
2 ✔
1608
            // we are not operational!
1609
            LOG.warn(e);
3 ✔
1610
            return;
1 ✔
1611
        }
1612

1613
        // notify any BrokerPoolServices that we are about to shutdown
1614
        try {
1615
            // instruct database services that we are about to stop multi-user mode
1616
            servicesManager.stopMultiUserServices(this);
4 ✔
1617
        } catch(final BrokerPoolServicesManagerException e) {
1 ✔
1618
            for(final BrokerPoolServiceException bpse : e.getServiceExceptions()) {
×
1619
                LOG.error(bpse.getMessage(), bpse);
×
1620
            }
1621
        }
1622

1623
        try {
1624
            status.process(Event.START_SHUTDOWN_SYSTEM_MODE);
5 ✔
1625
        } catch(final IllegalStateException e) {
1 ✔
1626
            // we are not in SHUTTING_DOWN_MULTI_USER_MODE!
1627
            LOG.warn(e);
×
1628
            return;
×
1629
        }
1630

1631
        try {
1632
            LOG.info("Database is shutting down ...");
3 ✔
1633

1634
            processMonitor.stopRunningJobs();
3 ✔
1635

1636
            // C3 fix (shutdown lifecycle audit, 2026-04-29):
1637
            // Bound scheduler shutdown so a stuck Quartz job cannot hang the JVM. Wait up to
1638
            // half of maxShutdownWait for jobs to complete; if the deadline expires, the
1639
            // implementation logs and force-interrupts running jobs, then shutdown(false)s.
1640
            // A non-positive maxShutdownWait disables the bound (semantics preserved for
1641
            // configurations that explicitly opted out via wait-before-shutdown).
1642
            final long schedulerTimeoutMs = maxShutdownWait > 0 ? maxShutdownWait / 2 : -1L;
11 !
1643
            if (schedulerTimeoutMs > 0) {
4 !
1644
                scheduler.shutdown(schedulerTimeoutMs);
4 ✔
1645
            } else {
1 ✔
1646
                scheduler.shutdown(true);
×
1647
            }
1648

1649
            try {
1650
                statusReporter = new StatusReporter(SIGNAL_SHUTDOWN);
6 ✔
1651
                statusObservers.forEach(statusReporter::addObserver);
9 ✔
1652

1653
                synchronized (this) {
7 ✔
1654
                    final Thread statusThread = newInstanceDaemonThread(this, "shutdown-status-reporter", statusReporter);
6 ✔
1655
                    statusThread.start();
2 ✔
1656

1657
                    // DW: only in debug mode
1658
                    if (LOG.isDebugEnabled()) {
3 !
1659
                        notificationService.debug();
×
1660
                    }
1661

1662
                    //Notify all running tasks that we are shutting down
1663

1664
                    //Notify all running XQueries that we are shutting down
1665
                    processMonitor.killAll(500);
4 ✔
1666

1667
                    if (isRecoveryEnabled()) {
3 ✔
1668
                        journalManager.ifPresent(jm -> jm.flush(true, true));
9 ✔
1669
                    }
1670

1671
                    final long waitStart = System.currentTimeMillis();
2 ✔
1672
                    //Are there active brokers ?
1673
                    if (!activeBrokers.isEmpty()) {
4 !
1674
                        printSystemInfo();
×
1675
                        LOG.info("Waiting {}ms for remaining threads to shut down...", maxShutdownWait);
×
1676
                        while (!activeBrokers.isEmpty()) {
×
1677
                            try {
1678
                                //Wait until they become inactive...
1679
                                this.wait(1000);
×
1680
                            } catch (final InterruptedException e) {
×
1681
                                Thread.currentThread().interrupt();
×
1682
                                LOG.warn("Interrupted while waiting for active brokers to return during shutdown");
×
1683
                                // C4 fix (shutdown lifecycle audit, 2026-04-29):
1684
                                // Surface holders so unflushed transactions are visible in the log
1685
                                // before we proceed into broker.shutdown() with leases still open.
1686
                                logActiveBrokerHolders("interrupt");
×
1687
                                break;
×
1688
                            }
1689

1690
                            //...or force the shutdown
1691
                            if (maxShutdownWait > -1 && System.currentTimeMillis() - waitStart > maxShutdownWait) {
×
1692
                                LOG.warn("Not all threads returned. Forcing shutdown ...");
×
1693
                                logActiveBrokerHolders("timeout");
×
1694
                                break;
×
1695
                            }
1696
                        }
1697
                    }
1698
                    LOG.debug("Calling shutdown ...");
3 ✔
1699

1700
                    //TODO : replace the following code by get()/release() statements ?
1701
                    // WM: deadlock risk if not all brokers returned properly.
1702
                    DBBroker broker = null;
2 ✔
1703
                    if (inactiveBrokers.isEmpty())
4 !
1704
                        try {
1705
                            broker = createBroker();
×
1706
                        } catch (final EXistException e) {
×
1707
                            LOG.warn("could not create instance for shutdown. Giving up.");
×
1708
                        }
×
1709
                    else
1710
                    //TODO : this broker is *not* marked as active and may be reused by another process !
1711
                    //TODO : use get() then release the broker ?
1712
                    // WM: deadlock risk if not all brokers returned properly.
1713
                    //TODO: always createBroker? -dmitriy
1714
                    {
1715
                        broker = inactiveBrokers.peek();
5 ✔
1716
                    }
1717

1718
                    try {
1719
                        if (broker != null) {
2 !
1720
                            broker.prepare();
2 ✔
1721
                            broker.pushSubject(securityManager.getSystemSubject());
5 ✔
1722
                        }
1723

1724
                        try {
1725
                            // instruct all database services to stop
1726
                            servicesManager.stopSystemServices(broker);
4 ✔
1727
                        } catch(final BrokerPoolServicesManagerException e) {
1 ✔
1728
                           for(final BrokerPoolServiceException bpse : e.getServiceExceptions()) {
×
1729
                               LOG.error(bpse.getMessage(), bpse);
×
1730
                           }
1731
                        }
1732

1733
                        //TOUNDERSTAND (pb) : shutdown() is called on only *one* broker ?
1734
                        // WM: yes, the database files are shared, so only one broker is needed to close them for all
1735
                        broker.shutdown();
2 ✔
1736

1737
                    } finally {
1 ✔
1738
                        if(broker != null) {
2 !
1739
                            broker.popSubject();
3 ✔
1740
                        }
1741
                    }
1742

1743
                    collectionCache.invalidateAll();
3 ✔
1744

1745
                    // final notification to database services to shutdown
1746
                    servicesManager.shutdown();
3 ✔
1747

1748
                    // remove all remaining inactive brokers as we have shutdown now and no longer need those
1749
                    inactiveBrokers.clear();
3 ✔
1750

1751
                    // deregister JMX MBeans
1752
                    AgentFactory.getInstance().closeDBInstance(this);
3 ✔
1753

1754
                    //Clear the living instances container
1755
                    shutdownInstanceConsumer.accept(instanceName);
4 ✔
1756
                    if (!readOnly.get()) {
4 !
1757
                        // release the lock on the data directory
1758
                        dataLock.release();
3 ✔
1759
                    }
1760

1761
                    //clearing additional resources, like ThreadLocal
1762
                    clearThreadLocals();
2 ✔
1763

1764
                    LOG.info("shutdown complete !");
3 ✔
1765

1766
                    if (shutdownListener != null) {
3 ✔
1767
                        shutdownListener.shutdown(instanceName, instancesCount());
6 ✔
1768
                    }
1769
                }
1770
            } finally {
1771
                // clear instance variables, just to be sure they will be garbage collected
1772
                // the test suite restarts the db a few hundred times
1773
                Configurator.clear(this);
2 ✔
1774
                transactionManager = null;
3 ✔
1775
                collectionCache = null;
3 ✔
1776
                xQueryPool = null;
3 ✔
1777
                processMonitor = null;
3 ✔
1778
                collectionConfigurationManager = null;
3 ✔
1779
                notificationService = null;
3 ✔
1780
                indexManager = null;
3 ✔
1781
                xmlReaderPool = null;
3 ✔
1782
                shutdownListener = null;
3 ✔
1783
                securityManager = null;
3 ✔
1784

1785
                if (lockManager != null) {
3 !
1786
                    lockManager.getLockTable().shutdown();
4 ✔
1787
                    lockManager = null;
3 ✔
1788
                }
1789

1790
                notificationService = null;
3 ✔
1791
                statusObservers.clear();
3 ✔
1792
                startupTriggersManager = null;
3 ✔
1793
                statusReporter.terminate();
3 ✔
1794
                statusReporter = null;
3 ✔
1795

1796
                // interrupt any remaining threads in the instance thread group
1797
                // to prevent the JVM from hanging on non-daemon threads
1798
                try {
1799
                    // C5 fix (shutdown lifecycle audit, 2026-04-29):
1800
                    // Surface any threads that survived the structured shutdown so leaks are
1801
                    // diagnosable. Each named thread here is a candidate for an explicit
1802
                    // BrokerPoolService.shutdown() rather than relying on this blanket
1803
                    // ThreadGroup.interrupt() fallback.
1804
                    logSurvivingInstanceThreads();
2 ✔
1805
                    instanceThreadGroup.interrupt();
3 ✔
1806
                } catch (final SecurityException e) {
1 ✔
1807
                    LOG.warn("Could not interrupt instance thread group: {}", e.getMessage());
×
1808
                }
1809
            }
1810
        } finally {
1811
            status.process(Event.FINISHED_SHUTDOWN);
5 ✔
1812
        }
1813
    }
1 ✔
1814

1815
    public void addStatusObserver(final Observer statusObserver) {
1816
        this.statusObservers.add(statusObserver);
×
1817
    }
×
1818

1819
    public boolean removeStatusObserver(final Observer statusObserver) {
1820
        return this.statusObservers.remove(statusObserver);
×
1821
    }
1822

1823
    private void clearThreadLocals() {
1824
        for (final Thread thread : Thread.getAllStackTraces().keySet()) {
12 ✔
1825
            try {
1826
                cleanThreadLocalsForThread(thread);
×
1827
            } catch (final EXistException ex) {
1 ✔
1828
                if (LOG.isDebugEnabled()) {
3 !
1829
                    LOG.warn("Could not clear ThreadLocals for thread: {}", thread.getName());
×
1830
                }
1831
            }
1832
        }
1833
    }
1 ✔
1834

1835
    private void cleanThreadLocalsForThread(final Thread thread) throws EXistException {
1836
        try {
1837
            // Get a reference to the thread locals table of the current thread
1838
            final Field threadLocalsField = Thread.class.getDeclaredField("threadLocals");
4 ✔
1839
            threadLocalsField.setAccessible(true);
×
1840
            final Object threadLocalTable = threadLocalsField.get(thread);
×
1841

1842
            // Get a reference to the array holding the thread local variables inside the
1843
            // ThreadLocalMap of the current thread
1844
            final Class threadLocalMapClass = Class.forName("java.lang.ThreadLocal$ThreadLocalMap");
×
1845
            final Field tableField = threadLocalMapClass.getDeclaredField("table");
×
1846
            tableField.setAccessible(true);
×
1847
            final Object table = tableField.get(threadLocalTable);
×
1848

1849
            // The key to the ThreadLocalMap is a WeakReference object. The referent field of this object
1850
            // is a reference to the actual ThreadLocal variable
1851
            final Field referentField = Reference.class.getDeclaredField("referent");
×
1852
            referentField.setAccessible(true);
×
1853

1854
            for (int i = 0; i < Array.getLength(table); i++) {
×
1855
                // Each entry in the table array of ThreadLocalMap is an Entry object
1856
                // representing the thread local reference and its value
1857
                final Object entry = Array.get(table, i);
×
1858
                if (entry != null) {
×
1859
                    // Get a reference to the thread local object and remove it from the table
1860
                    final ThreadLocal threadLocal = (ThreadLocal)referentField.get(entry);
×
1861
                    threadLocal.remove();
×
1862
                }
1863
            }
1864
        } catch(final Exception e) {
1 ✔
1865
            // We will tolerate an exception here and just log it
1866
            throw new EXistException(e);
5 ✔
1867
        }
1868
    }
×
1869

1870
    public Optional<BrokerWatchdog> getWatchdog() {
1871
        return watchdog;
×
1872
    }
1873

1874
    //TODO : move this elsewhere
1875
    public void triggerCheckpoint() {
1876
        if(syncRequired) {
3 ✔
1877
            return;
1 ✔
1878
        }
1879
        synchronized(this) {
7 ✔
1880
            syncEvent = Sync.MAJOR;
3 ✔
1881
            syncRequired = true;
3 ✔
1882
            checkpoint = true;
3 ✔
1883
        }
1884
    }
1 ✔
1885

1886
    private Debuggee debuggee = null;
3 ✔
1887

1888
    public Debuggee getDebuggee() {
1889
        synchronized(this) {
×
1890
            if(debuggee == null) {
×
1891
                debuggee = DebuggeeFactory.getInstance();
×
1892
            }
1893
        }
1894

1895
        return debuggee;
×
1896
    }
1897

1898
    public Calendar getStartupTime() {
1899
        return startupTime;
3 ✔
1900
    }
1901

1902
    public void printSystemInfo() {
1903
        try(final StringWriter sout = new StringWriter();
×
1904
            final PrintWriter writer = new PrintWriter(sout)) {
×
1905

1906
            writer.println("SYSTEM INFO");
×
1907
            writer.format("Database instance: %s\n", getId());
×
1908
            writer.println("-------------------------------------------------------------------");
×
1909
            watchdog.ifPresent(wd -> wd.dump(writer));
×
1910

1911
            final String s = sout.toString();
×
1912
            LOG.info(s);
×
1913
            System.err.println(s);
×
1914
        } catch(final IOException e) {
×
1915
            LOG.error(e);
×
1916
        }
1917
    }
×
1918

1919
    /**
1920
     * Enumerate threads still alive in {@code instanceThreadGroup} immediately before the
1921
     * blanket {@code interrupt()} fallback, and log them by name and daemon status.
1922
     *
1923
     * Audit finding C5 (2026-04-29): {@code ThreadGroup.interrupt()} is a band-aid for any
1924
     * subsystem that did not register an explicit shutdown via {@code BrokerPoolService}.
1925
     * Logging the survivors makes the leak visible so it can be fixed at source rather than
1926
     * masked by the interrupt.
1927
     */
1928
    private void logSurvivingInstanceThreads() {
1929
        try {
1930
            final Thread[] active = new Thread[instanceThreadGroup.activeCount() + 8];
7 ✔
1931
            final int found = instanceThreadGroup.enumerate(active, true);
6 ✔
1932
            for (int i = 0; i < found; i++) {
7 ✔
1933
                final Thread t = active[i];
4 ✔
1934
                if (t == null || t == Thread.currentThread() || !t.isAlive()) {
8 !
1935
                    continue;
1 ✔
1936
                }
1937
                LOG.warn("Thread '{}' (state={}, daemon={}) still alive in instance thread group at shutdown — will be interrupted",
3 ✔
1938
                        t.getName(), t.getState(), t.isDaemon());
7 ✔
1939
            }
1940
        } catch (final SecurityException e) {
1 ✔
1941
            LOG.warn("Could not enumerate instance thread group at shutdown: {}", e.getMessage());
×
1942
        }
1943
    }
1 ✔
1944

1945
    /**
1946
     * Log every thread that still holds an active broker, with its current stack trace.
1947
     *
1948
     * Used by the shutdown sequence (audit finding C4) when the {@code activeBrokers} drain
1949
     * is interrupted or times out — proceeding with {@code broker.shutdown()} while leases
1950
     * are still open risks unflushed dirty pages, so making the holders visible is critical
1951
     * for diagnosing the cause on next start.
1952
     *
1953
     * @param reason short tag included in the log message ({@code "interrupt"} or {@code "timeout"}).
1954
     */
1955
    private void logActiveBrokerHolders(final String reason) {
1956
        if (activeBrokers.isEmpty()) {
×
1957
            return;
×
1958
        }
1959
        LOG.warn("FORCED SHUTDOWN ({}) with {} active broker(s) — recovery may be required on next start",
×
1960
                reason, activeBrokers.size());
×
1961
        for (final Map.Entry<Thread, DBBroker> entry : activeBrokers.entrySet()) {
×
1962
            final Thread holder = entry.getKey();
×
1963
            final StringBuilder sb = new StringBuilder();
×
1964
            sb.append("Thread '").append(holder.getName()).append("' (state=").append(holder.getState())
×
1965
                    .append(", daemon=").append(holder.isDaemon()).append(") still holds a broker:\n");
×
1966
            for (final StackTraceElement frame : holder.getStackTrace()) {
×
1967
                sb.append("\tat ").append(frame).append('\n');
×
1968
            }
1969
            LOG.warn(sb.toString());
×
1970
        }
1971
    }
×
1972

1973
    @ThreadSafe
1974
    private static class StatusReporter extends Observable implements Runnable {
1975
        private String status;
1976
        private volatile boolean terminate = false;
3 ✔
1977

1978
        public StatusReporter(final String status) {
2 ✔
1979
            this.status = status;
3 ✔
1980
        }
1 ✔
1981

1982
        public synchronized void setStatus(final String status) {
1983
            this.status = status;
3 ✔
1984
            this.setChanged();
2 ✔
1985
            this.notifyObservers(status);
3 ✔
1986
        }
1 ✔
1987

1988
        public void terminate() {
1989
            this.terminate = true;
3 ✔
1990
            synchronized(this) {
7 ✔
1991
                this.notifyAll();
2 ✔
1992
            }
1993
        }
1 ✔
1994

1995
        @Override
1996
        public void run() {
1997
            while(!terminate) {
4 ✔
1998
                synchronized(this) {
7 ✔
1999
                    try {
2000
                        wait(500);
3 ✔
2001
                    } catch(final InterruptedException e) {
1 ✔
2002
                        // nothing to do
2003
                    }
2004
                    this.setChanged();
2 ✔
2005
                    this.notifyObservers(status);
4 ✔
2006
                }
2007
            }
2008
        }
1 ✔
2009
    }
2010

2011
    @Override
2012
    public Path getStoragePlace() {
2013
        return (Path)conf.getProperty(BrokerPool.PROPERTY_DATA_DIR);
×
2014
    }
2015

2016
    private final List<TriggerProxy<? extends DocumentTrigger>> documentTriggers = new ArrayList<>();
5 ✔
2017
    private final List<TriggerProxy<? extends CollectionTrigger>> collectionTriggers = new ArrayList<>();
5 ✔
2018

2019
    @Override
2020
    public List<TriggerProxy<? extends DocumentTrigger>> getDocumentTriggers() {
2021
        return documentTriggers;
3 ✔
2022
    }
2023

2024
    @Override
2025
    public List<TriggerProxy<? extends CollectionTrigger>> getCollectionTriggers() {
2026
        return collectionTriggers;
3 ✔
2027
    }
2028

2029
    @Override
2030
    public void registerDocumentTrigger(final Class<? extends DocumentTrigger> clazz) {
2031
        documentTriggers.add(new DocumentTriggerProxy(clazz));
8 ✔
2032
    }
1 ✔
2033

2034
    @Override
2035
    public void registerCollectionTrigger(final Class<? extends CollectionTrigger> clazz) {
2036
        collectionTriggers.add(new CollectionTriggerProxy(clazz));
×
2037
    }
×
2038

2039
    public net.sf.saxon.Configuration getSaxonConfiguration() {
2040
        return saxonConfiguration.get().getConfiguration();
6 ✔
2041
    }
2042

2043
    public net.sf.saxon.s9api.Processor getSaxonProcessor() {
2044
        return saxonConfiguration.get().getProcessor();
6 ✔
2045
    }
2046

2047
    /**
2048
     * Represents a change involving {@link BrokerPool#inactiveBrokers}
2049
     * or {@link BrokerPool#activeBrokers} or {@link DBBroker#getReferenceCount}
2050
     *
2051
     * Used for tracing broker leases
2052
     */
2053
    private static class TraceableBrokerLeaseChange extends TraceableStateChange<TraceableBrokerLeaseChange.BrokerInfo, TraceableBrokerLeaseChange.Change> {
2054
        public enum Change {
×
2055
            GET,
×
2056
            RELEASE
×
2057
        }
2058

2059
        public static class BrokerInfo {
2060
            final String id;
2061
            final int referenceCount;
2062

2063
            public BrokerInfo(final String id, final int referenceCount) {
×
2064
                this.id = id;
×
2065
                this.referenceCount = referenceCount;
×
2066
            }
×
2067
        }
2068

2069
        private TraceableBrokerLeaseChange(final Change change, final BrokerInfo brokerInfo) {
2070
            super(change, brokerInfo);
×
2071
        }
×
2072

2073
        @Override
2074
        public String getId() {
2075
            return getState().id;
×
2076
        }
2077

2078
        @Override
2079
        public String describeState() {
2080
            return Integer.toString(getState().referenceCount);
×
2081
        }
2082

2083
        static TraceableBrokerLeaseChange get(final BrokerInfo brokerInfo) {
2084
            return new TraceableBrokerLeaseChange(Change.GET, brokerInfo);
×
2085
        }
2086

2087
        static TraceableBrokerLeaseChange release(final BrokerInfo brokerInfo) {
2088
            return new TraceableBrokerLeaseChange(Change.RELEASE, brokerInfo);
×
2089
        }
2090
    }
2091
}
2092

STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc