Introduce HikariCP as connection pool

Squash commits of branch feature/hikari:

- Introduce HikariCP as connection pool

- Log change

- Close maintenance stores correctly

- Do not force minimum number of connections

- Log change

- Assert stores are closed correctly

... and test QueryableStoreDeletionHandler

- Fix license

- Fix unit tests

- Use constants

- Change change log

- Move hikari version to libraries

- Enhance documentation

- Use configuration for pool settings
This commit is contained in:
Rene Pfeuffer
2025-06-19 09:41:03 +02:00
parent 5fe26b8092
commit 4b49748561
15 changed files with 610 additions and 106 deletions

View File

@@ -214,22 +214,37 @@ webapp:
workingCopyPoolStrategy: sonia.scm.repository.work.SimpleCachingWorkingCopyPool
## Amount of "cached" working copies
workingCopyPoolSize: 5
## Settings for queryable stores, which are backed by an SQLite database.
## Timeouts and lifetimes are in seconds
queryableStores:
maxPoolSize: 10
connectionTimeout: 30
idleTimeout: 600
maxLifetime: 1800
leakDetectionThreshold: 30
```
**Environment variables**
| Environment Variable | Corresponding config.yml property | Example |
| ----------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------ |
| SCM_WEBAPP_WORKDIR | webapp.workDir | export SCM_WEBAPP_WORKDIR=/tmp/scm-work |
| SCM_WEBAPP_HOMEDIR | webapp.homeDir | export SCM_WEBAPP_HOMEDIR=/var/lib/scm |
| SCM_WEBAPP_CACHE_DATAFILE_ENABLED | webapp.cache.datafile.enabled | export SCM_WEBAPP_CACHE_DATAFILE_ENABLED=true |
| SCM_WEBAPP_CACHE_STORE_ENABLED | webapp.cache.store.enabled | export SCM_WEBAPP_CACHE_STORE_ENABLED=true |
| SCM_WEBAPP_ENDLESSJWT | webapp.endlessJwt | export SCM_WEBAPP_ENDLESSJWT=false |
| SCM_WEBAPP_ASYNCTHREADS | webapp.asyncThreads | export SCM_WEBAPP_ASYNCTHREADS=4 |
| SCM_WEBAPP_MAXASYNCABORTSECONDS | webapp.maxAsyncAbortSeconds | export SCM_WEBAPP_MAXASYNCABORTSECONDS=60 |
| SCM_WEBAPP_CENTRALWORKQUEUE_WORKERS | webapp.centralWorkQueue.workers | export SCM_WEBAPP_CENTRALWORKQUEUE_WORKERS=4 |
| SCM_WEBAPP_WORKINGCOPYPOOLSTRATEGY | webapp.workingCopyPoolStrategy | export SCM_WEBAPP_WORKINGCOPYPOOLSTRATEGY=sonia.scm.repository.work.SimpleCachingWorkingCopyPool |
| SCM_WEBAPP_WORKINGCOPYPOOLSIZE | webapp.workingCopyPoolSize | export SCM_WEBAPP_WORKINGCOPYPOOLSIZE=5 |
| SCM_WEBAPP_INITIALUSER | webapp.initialUser | export SCM_WEBAPP_INITIALUSER=scmadmin |
| SCM_WEBAPP_INITIALPASSWORD | webapp.initialPassword | export SCM_WEBAPP_INITIALPASSWORD=scmadmin |
| SCM_WEBAPP_SKIPADMINCREATION | webapp.skipAdminCreation | export SCM_WEBAPP_SKIPADMINCREATION=true |
| Environment Variable | Corresponding config.yml property | Example |
|---------------------------------------------------|-----------------------------------------------|--------------------------------------------------------------------------------------------------|
| SCM_WEBAPP_WORKDIR | webapp.workDir | export SCM_WEBAPP_WORKDIR=/tmp/scm-work |
| SCM_WEBAPP_HOMEDIR | webapp.homeDir | export SCM_WEBAPP_HOMEDIR=/var/lib/scm |
| SCM_WEBAPP_CACHE_DATAFILE_ENABLED | webapp.cache.datafile.enabled | export SCM_WEBAPP_CACHE_DATAFILE_ENABLED=true |
| SCM_WEBAPP_CACHE_STORE_ENABLED | webapp.cache.store.enabled | export SCM_WEBAPP_CACHE_STORE_ENABLED=true |
| SCM_WEBAPP_ENDLESSJWT | webapp.endlessJwt | export SCM_WEBAPP_ENDLESSJWT=false |
| SCM_WEBAPP_ASYNCTHREADS | webapp.asyncThreads | export SCM_WEBAPP_ASYNCTHREADS=4 |
| SCM_WEBAPP_MAXASYNCABORTSECONDS | webapp.maxAsyncAbortSeconds | export SCM_WEBAPP_MAXASYNCABORTSECONDS=60 |
| SCM_WEBAPP_CENTRALWORKQUEUE_WORKERS | webapp.centralWorkQueue.workers | export SCM_WEBAPP_CENTRALWORKQUEUE_WORKERS=4 |
| SCM_WEBAPP_WORKINGCOPYPOOLSTRATEGY | webapp.workingCopyPoolStrategy | export SCM_WEBAPP_WORKINGCOPYPOOLSTRATEGY=sonia.scm.repository.work.SimpleCachingWorkingCopyPool |
| SCM_WEBAPP_WORKINGCOPYPOOLSIZE | webapp.workingCopyPoolSize | export SCM_WEBAPP_WORKINGCOPYPOOLSIZE=5 |
| SCM_WEBAPP_INITIALUSER | webapp.initialUser | export SCM_WEBAPP_INITIALUSER=scmadmin |
| SCM_WEBAPP_INITIALPASSWORD | webapp.initialPassword | export SCM_WEBAPP_INITIALPASSWORD=scmadmin |
| SCM_WEBAPP_SKIPADMINCREATION | webapp.skipAdminCreation | export SCM_WEBAPP_SKIPADMINCREATION=true |
| SCM_WEBAPP_QUERYABLESTORES_MAXPOOLSIZE | webapp.queryableStores.maxPoolSize | export SCM_WEBAPP_QUERYABLESTORES_MAXPOOLSIZE=20 |
| SCM_WEBAPP_QUERYABLESTORES_CONNECTIONTIMEOUT | webapp.queryableStores.connectionTimeout | export SCM_WEBAPP_QUERYABLESTORES_CONNECTIONTIMEOUT=30 |
| SCM_WEBAPP_QUERYABLESTORES_IDLETIMEOUT | webapp.queryableStores.idleTimeout | export SCM_WEBAPP_QUERYABLESTORES_IDLETIMEOUT=600 |
| SCM_WEBAPP_QUERYABLESTORES_MAXLIFETIME | webapp.queryableStores.maxLifetime | export SCM_WEBAPP_QUERYABLESTORES_MAXLIFETIME=1800 |
| SCM_WEBAPP_QUERYABLESTORES_LEAKDETECTIONTHRESHOLD | webapp.queryableStores.leakDetectionThreshold | export SCM_WEBAPP_QUERYABLESTORES_LEAKDETECTIONTHRESHOLD=1800 |

View File

@@ -78,6 +78,13 @@ however, specific methods are created here based on the parent classes mentioned
To create and change data, specific IDs must be specified to access the store if parent classes have been defined. This
store implements the known Store API (the `DataStore` interface), so no adjustments are needed in the application.
Since the new persistence layer is based on SQL, the stored have to be closed after use. This is done best with
a try-with-resources block. If the store is not closed, the connection to the database will not be released,
which can lead to performance issues and memory leaks that will let the application crash in the long run.
If stores are not closed, the application will log a warning message. The best way to avoid this is to use
unit tests with the `QueryableStoreExtension`, which will assert that all stores are closed correctly in the tested
code.
### Queryable Store
For more advanced queries that also extend beyond the boundaries of the parent classes, there is a new store with a new
@@ -308,33 +315,38 @@ public class Demo {
entity.setAge(age);
entity.setTags(tags);
QueryableMutableStore<MyEntity> store = storeFactory.getMutable();
return store.put(entity);
try (QueryableMutableStore<MyEntity> store = storeFactory.getMutable()) {
return store.put(entity);
}
}
public MyEntity readById(String id) {
QueryableMutableStore<MyEntity> store = storeFactory.getMutable();
return store.get(id);
try (QueryableMutableStore<MyEntity> store = storeFactory.getMutable()) {
return store.get(id);
}
}
public Collection<MyEntity> findByAge(int age) {
QueryableStore<MyEntity> store = storeFactory.get();
return store.query(MyEntityQueryFields.AGE.eq(age)).findAll();
try (QueryableStore<MyEntity> store = storeFactory.get()) {
return store.query(MyEntityQueryFields.AGE.eq(age)).findAll();
}
}
public Collection<MyEntity> findByName(String name) {
QueryableStore<MyEntity> store = storeFactory.get();
return store.query(
Conditions.or(
MyEntityQueryFields.NAME.eq(name),
MyEntityQueryFields.ALIAS.eq(name)
)
).findAll();
try (QueryableStore<MyEntity> store = storeFactory.get()) {
return store.query(
Conditions.or(
MyEntityQueryFields.NAME.eq(name),
MyEntityQueryFields.ALIAS.eq(name)
)
).findAll();
}
}
public Collection<MyEntity> findByTag(String tag) {
QueryableStore<MyEntity> store = storeFactory.get();
return store.query(MyEntityQueryFields.TAGS.contains(tag)).findAll();
try (QueryableStore<MyEntity> store = storeFactory.get()) {
return store.query(MyEntityQueryFields.TAGS.contains(tag)).findAll();
}
}
}
```
@@ -364,22 +376,25 @@ public class Demo {
}
public void addContact(User user, String mail) {
QueryableMutableStore<Contact> store = storeFactory.getMutable(user);
Contact contact = new Contact();
contact.setMail(mail);
store.put(contact);
try (QueryableMutableStore<Contact> store = storeFactory.getMutable(user)) {
store.put(contact);
}
}
/** Get contact for a single user. */
public Collection<Contact> getContacts(User user) {
QueryableMutableStore<Contact> store = storeFactory.getMutable(user);
return store.getAll().values();
try (QueryableMutableStore<Contact> store = storeFactory.getMutable(user)) {
return store.getAll().values();
}
}
/** Get all contacts for all users. */
public Collection<Contact> getAllContacts() {
QueryableStore<Contact> store = storeFactory.getOverall();
return store.query().findAll();
try (QueryableStore<Contact> store = storeFactory.getOverall()) {
return store.query().findAll();
}
}
}
```
@@ -481,6 +496,7 @@ public class Contact {
The following update step can be used to add the `type` field to all `Contact` entities:
```java
@Extension
public class AddTypeToContactsUpdateStep implements UpdateStep {
@@ -493,8 +509,9 @@ public class AddTypeToContactsUpdateStep implements UpdateStep {
@Override
public void doUpdate() {
try (MaintenanceIterator<Contact> iter = updateStepUtilFactory.forQueryableType(Contact.class).iterateAll()) {
while(iter.hasNext()) {
try (sonia.scm.store.QueryableMaintenanceStore<Object> store = updateStepUtilFactory.forQueryableType(Contact.class);
MaintenanceIterator<Contact> iter = store.iterateAll()) {
while (iter.hasNext()) {
MaintenanceStoreEntry<Contact> entry = iter.next();
Contact contact = entry.get();
contact.setType("personal");