Hello Keycloak Developers,
Infinispan supports storing cache contents in a database via the jdbc-store module, however as of Keycloak 15.0.2 a write-through is not possible
with Keycloak because the code in `org.keycloak.models.sessions.infinispan.changes.InfinispanChangelogBasedTransaction` is configured to explicitly
skip the write to any configured cache-store via `CacheDecorators.skipCacheStore(cache)`.
If one could configure `InfinispanChangelogBasedTransaction` to propagate cache changes to a cache store, if a cache store is present for a cache,
then one could store cache data in an external store, which survives restarts without the need for an external infinispan cluster.
As a PoC I did a small patch to the `org.keycloak.models.sessions.infinispan.CacheDecorators` and configured a jdbc-store for the user `sessions` cache:
```
public class CacheDecorators {
private static final boolean IGNORE_SKIP_CACHE_STORE=Boolean.getBoolean("keycloak.infinispan.ignoreSkipCacheStore");
...
public static <K, V> AdvancedCache<K, V> skipCacheStore(Cache<K, V> cache) {
if (IGNORE_SKIP_CACHE_STORE) {
return cache.getAdvancedCache();
}
return cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_STORE);
}
}
```
If I set the system property `-Dkeycloak.infinispan.ignoreSkipCacheStore=true` when I start a Keycloak instance, then cache writes are propagated to
the configured cache-store.
```
<distributed-cache name="sessions" owners="1">
<expiration lifespan="900000000000000000"/>
<jdbc-store data-source="KeycloakDS" max-batch-size="1000" fetch-state="true" passivation="false" preload="false" purge="false" shared="true">
<property name="databaseType">POSTGRES</property>
<table fetch-size="5000" drop-on-stop="false" prefix="ispn">
<data-column type="bytea"/>
</table>
</jdbc-store>
</distributed-cache>
```
The following example .cli script shows how to configure a JDBC cache store for the user sessions cache that is backed by a postgres database.
Note that we use the KeycloakDS datasource here.
0300-onstart-setup-ispn-jdbc-store.cli:
```
embed-server --server-config=${env.KEYCLOAK_CONFIG_FILE:standalone-ha.xml} --std-out=echo
echo Using server configuration file:
:resolve-expression(expression=${env.JBOSS_HOME}/standalone/configuration/${env.KEYCLOAK_CONFIG_FILE:standalone-ha.xml})
echo SETUP: Begin Infinispan jdbc-store configuration.
/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:remove()
batch
/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:add(owners=1)
/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions/component=expiration:add(lifespan=900000000000000000)
/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions/component=eviction:add(max-entries=1)
/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions/store=jdbc:add( \
datasource="java:jboss/datasources/KeycloakDS", \
passivation=false, \
fetch-state=true, \
preload=false, \
purge=false, \
shared=true, \
max-batch-size=1000 \
)
/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions/store=jdbc:write-attribute( \
name=properties.databaseType, \
value=POSTGRES \
)
/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions/store=jdbc/table=string:add( \
data-column={type=bytea}, \
drop-on-stop=false, \
fetch-size=5000, \
prefix=ispn \
)
/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions/store=jdbc/write=behind:add( \
modification-queue-size=1024 \
)
run-batch
echo SETUP: Finished Infinispan jdbc-store configuration.
stop-embedded-server
```
With this in place, Keycloak will create a table called: ispn_sessions if missing where the session contents are stored.
When a user signs in, an entry is added to the table. When the user signs out again, then the entry is removed from the table.
Multiple Keycloak instances can access the session information. If I stop all Keycloak instances and restart the cluster, all users are still correctly logged in.
The usual way to achieve this is to use an external infinispan cluster. However, this introduces a new level of complexity.
The configuration shown above is IMHO much more straightforward and is probably enough for most use cases.
Is there anything against making the above "skipCacheStore" functionality configurable via a configuration setting on the
`org.keycloak.models.sessions.infinispan.InfinispanUserSessionProviderFactory` like `allowCacheStore=true`. This can then be passed down
to `org.keycloak.models.sessions.infinispan.InfinispanUserSessionProvider` to the `InfinispanChangelogBasedTransaction`.
There we could wrap all calls to `CacheDecorators.skipCacheStore(cache)` with a method like `getDecoratedAdvancedCache()` which does:
```
protected AdvancedCache<K, SessionEntityWrapper<V>> getDecoratedAdvancedCache() {
if (allowCacheStore) {
return cache.getAdvancedCache();
}
return CacheDecorators.skipCacheStore(cache);
}
```
What do you guys think?
Cheers,
Thomas