Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c46f3864ce
|
||
|
|
e19beedc14 | ||
|
|
fc92c24132 | ||
|
|
49f87072eb | ||
|
|
703e16d849 | ||
|
|
c78ab8c6b7 | ||
|
|
a9adc28792
|
No files matched your search
@@ -1,16 +1,14 @@
|
||||
version: 2.1
|
||||
setup: true
|
||||
orbs:
|
||||
qqq-orb: kingsrook/qqq-orb@0.7.18
|
||||
qqq-orb: kingsrook/qqq-orb@0.8
|
||||
workflows:
|
||||
setup:
|
||||
jobs:
|
||||
- qqq-orb/generate_pipeline:
|
||||
orb_version: 0.7.18
|
||||
orb_version: '0.8'
|
||||
filters:
|
||||
branches:
|
||||
ignore: automation/qbit-rc-pending
|
||||
tags:
|
||||
only:
|
||||
- /^v\d+\.\d+\.\d+$/
|
||||
- /publish.*/
|
||||
only: /^v\d+\.\d+\.\d+$/
|
||||
+1
-2
@@ -1,5 +1,5 @@
|
||||
profile: qbit
|
||||
orb_version: 0.7.18
|
||||
orb_version: '0.8'
|
||||
default_branch: develop
|
||||
java_version: '21'
|
||||
browsers: true
|
||||
@@ -10,7 +10,6 @@ release_tag_policy: stable
|
||||
test_branch_policy: non-release
|
||||
publish:
|
||||
snapshot: true
|
||||
feature: true
|
||||
release_candidate: true
|
||||
release: true
|
||||
asciidoc: false
|
||||
|
||||
@@ -138,53 +138,6 @@ new QSessionStoreQBitProducer()
|
||||
.produce(qInstance);
|
||||
```
|
||||
|
||||
### Shared Redis infrastructure
|
||||
|
||||
Use `SHARED_REDIS` when the application installs `qbit-redis`. Include that optional
|
||||
artifact explicitly in the host and produce its named connection metadata before
|
||||
session-store. The Redis runtime owns TLS, credentials, bounded pools and health.
|
||||
Session-store owns JSON serialization, session expiry and payload limits.
|
||||
|
||||
```java
|
||||
new QSessionStoreQBitProducer()
|
||||
.withConfig(new QSessionStoreQBitConfig()
|
||||
.withProviderType(QSessionStoreProviderType.SHARED_REDIS)
|
||||
.withSharedRedis(new SharedRedisSessionStoreSettings(
|
||||
"sessions", "production", "orders-app", "authentication", 65536))
|
||||
.withDefaultTtl(Duration.ofHours(1))
|
||||
.withEnableSlidingExpiration(true))
|
||||
.produce(qInstance);
|
||||
```
|
||||
|
||||
Settings are connection name, deployment, application, authentication-domain scope,
|
||||
and maximum serialized bytes. Nodes serving the same application/domain must use
|
||||
identical scope; distinct applications/domains must use distinct scope. Resolve it
|
||||
from host configuration, never an unauthenticated request. Redis namespacing encodes
|
||||
each segment independently. This provider does not interpret per-user tenant claims.
|
||||
|
||||
Operations require the owning Redis runtime to be started. The provider never opens
|
||||
or closes a pool and never falls back to another connection. `loadAndTouch` uses
|
||||
atomic GETEX when sliding is enabled, plain GET otherwise; Redis 6.2+ is required.
|
||||
Missing values are cache misses; infrastructure, size and decoding failures throw.
|
||||
Failed remove also throws so logout can retry. TTLs must be 1 ms through 365 days;
|
||||
serialized payload bounds may be configured up to 1 MiB. No cleanup job is produced
|
||||
because Redis owns expiry. Framework Redis and session-store health checks report
|
||||
availability; the provider's status string is only descriptive.
|
||||
|
||||
The existing `REDIS` provider retains its independent legacy pool/configuration.
|
||||
Selecting `SHARED_REDIS` is explicit and uses a new namespace; it does not migrate
|
||||
existing cache entries. A framework runtime service releases its owned registry/context entries in
|
||||
`afterApplicationStop`, after dependent services have stopped; it never closes
|
||||
the Redis pool. Repeated cleanup is harmless and preserves replacement providers.
|
||||
Restart reconstructs registration from metadata after Redis starts. Manual hosts
|
||||
must honor the same runtime callbacks. Shared providers weakly reference their
|
||||
application so an abandoned pre-start registration cannot retain the owner.
|
||||
|
||||
Local acceptance covers real HTTP authentication/logout and Redis outage/retry
|
||||
through the core sample's `redis-acceptance` profile. Coordinated artifact
|
||||
publication and clean host adoption remain release gates. Redis failover topology
|
||||
qualification is future work tracked in [QQQ #1301](https://github.com/QRun-IO/qqq/issues/1301).
|
||||
|
||||
### Custom Provider
|
||||
|
||||
```java
|
||||
@@ -197,18 +150,10 @@ new QSessionStoreQBitProducer()
|
||||
|
||||
## Accessing the Provider
|
||||
|
||||
After producing the QBit, retrieve the provider for its owning application.
|
||||
Within a request, `getProvider()` uses `QContext.getQInstance()`. Startup and
|
||||
background callers should use the explicit overload below. The qbit automatically
|
||||
registers against the QInstance supplied to `produce`; it never installs a
|
||||
JVM-wide fallback for other applications.
|
||||
|
||||
Hosts registering directly with core must use
|
||||
`QSessionStoreRegistry.getInstance(qInstance).register(provider)` outside request
|
||||
context. See [registration migration](docs/SESSION-REGISTRATION.md).
|
||||
After producing the QBit, retrieve the provider via context:
|
||||
|
||||
```java
|
||||
QSessionStoreProviderInterface provider = QSessionStoreQBitContext.getProvider(qInstance);
|
||||
QSessionStoreProviderInterface provider = QSessionStoreQBitContext.getProvider();
|
||||
|
||||
// Store a session
|
||||
provider.store(session.getUuid(), session, Duration.ofHours(1));
|
||||
@@ -230,8 +175,3 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
||||
## License
|
||||
|
||||
Apache-2.0 - See [LICENSE](LICENSE)
|
||||
|
||||
Launcher acceptance covers normal shutdown, failure of a later runtime, and Redis
|
||||
authentication failure before session-runtime startup followed by retry. In that
|
||||
early failure case no Redis pool is published; the weak pre-start registration
|
||||
remains configured but cannot execute session operations until Redis starts.
|
||||
@@ -1,34 +0,0 @@
|
||||
# Application-scoped session store registration
|
||||
|
||||
The candidate core registry resolves the QInstance in QContext. Application
|
||||
startup must register explicitly, even when no request context exists:
|
||||
|
||||
```java
|
||||
QSessionStoreRegistry.getInstance(qInstance).register(provider);
|
||||
```
|
||||
|
||||
Calls to `getInstance()` within a request use that request's application. Without
|
||||
a QInstance, that call accesses a separate standalone registry. An application
|
||||
never inherits that standalone provider. Existing hosts that register outside
|
||||
QContext must migrate to the explicit overload alongside this core candidate.
|
||||
Deploy matching session-store/core versions; this is not a transparent change for
|
||||
old context-free startup integrations.
|
||||
|
||||
Session-store's producer now supplies its target QInstance to both core registration
|
||||
and qbit context. Context getters/setters/clear operate on the current application,
|
||||
with explicit QInstance overloads for metadata production/background operations.
|
||||
Health metadata selects its target application and the health probe uses the
|
||||
QInstance in HealthCheckInput (falling back to current context only if omitted).
|
||||
|
||||
Regression reproduced before the fix: after application A stored a session,
|
||||
unconfigured application B loaded that same session through QSessionStoreHelper.
|
||||
The test now checks independent same-ID sessions, isolation of clearing, and health
|
||||
checks for an explicit target while a different application is on the caller thread.
|
||||
|
||||
Hosts sharing a Redis server must configure separate application/authentication
|
||||
domain namespaces. The `SHARED_REDIS` provider uses those explicit settings and
|
||||
the owning Redis QBit's pool. Its runtime releases owned registrations on stop
|
||||
and reconstructs them on restart. Local acceptance covers launcher lifecycle,
|
||||
HTTP authentication/logout and Redis outage/retry; coordinated published-artifact
|
||||
and clean-host verification remain release gates. See the shared Redis setup in
|
||||
[README](../README.md).
|
||||
@@ -21,8 +21,7 @@
|
||||
<revision>0.1.0-SNAPSHOT</revision>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<jedis.version>7.2.0</jedis.version>
|
||||
<qqq.snapshot.version>4.1.0-collab-33464d37e-SNAPSHOT</qqq.snapshot.version>
|
||||
<jedis.version>5.1.0</jedis.version>
|
||||
</properties>
|
||||
|
||||
<parent>
|
||||
@@ -79,7 +78,7 @@
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-bom-pom</artifactId>
|
||||
<version>${qqq.snapshot.version}</version>
|
||||
<version>4.1.0-RC.18</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
@@ -92,13 +91,6 @@
|
||||
<artifactId>qqq-backend-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qbits</groupId>
|
||||
<artifactId>qbit-redis</artifactId>
|
||||
<version>0.1.0-redis-7fe5492-SNAPSHOT</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Redis client (optional - for RedisSessionStoreProvider) -->
|
||||
<dependency>
|
||||
<groupId>redis.clients</groupId>
|
||||
@@ -108,12 +100,6 @@
|
||||
</dependency>
|
||||
|
||||
<!-- Test dependencies -->
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-middleware-javalin</artifactId>
|
||||
<version>${qqq.snapshot.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-backend-module-rdbms</artifactId>
|
||||
@@ -152,13 +138,13 @@
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>testcontainers</artifactId>
|
||||
<version>2.0.3</version>
|
||||
<version>1.19.3</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>testcontainers-junit-jupiter</artifactId>
|
||||
<version>2.0.3</version>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>1.19.3</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
@@ -18,12 +18,9 @@ package com.kingsrook.qbits.sessionstore;
|
||||
|
||||
import com.kingsrook.qbits.sessionstore.providers.InMemorySessionStoreProvider;
|
||||
import com.kingsrook.qbits.sessionstore.providers.RedisSessionStoreProvider;
|
||||
import com.kingsrook.qbits.sessionstore.providers.SharedRedisSessionStoreProvider;
|
||||
import com.kingsrook.qbits.sessionstore.providers.TableBasedSessionStoreProvider;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@@ -36,23 +33,12 @@ public class QSessionStoreProviderFactory
|
||||
** Create a provider instance based on the configured type.
|
||||
***************************************************************************/
|
||||
public static QSessionStoreProviderInterface createProvider(QSessionStoreQBitConfig config) throws QException
|
||||
{
|
||||
return createProvider(QContext.getQInstance(), config);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Bind providers needing managed infrastructure to their owning application.
|
||||
***************************************************************************/
|
||||
public static QSessionStoreProviderInterface createProvider(QInstance instance, QSessionStoreQBitConfig config) throws QException
|
||||
{
|
||||
QSessionStoreProviderInterface provider = switch(config.getProviderType())
|
||||
{
|
||||
case IN_MEMORY -> new InMemorySessionStoreProvider();
|
||||
case TABLE_BASED -> new TableBasedSessionStoreProvider();
|
||||
case REDIS -> new RedisSessionStoreProvider();
|
||||
case SHARED_REDIS -> new SharedRedisSessionStoreProvider(instance);
|
||||
case CUSTOM -> QCodeLoader.getAdHoc(QSessionStoreProviderInterface.class, config.getCustomProviderCodeReference());
|
||||
};
|
||||
|
||||
|
||||
@@ -24,6 +24,5 @@ public enum QSessionStoreProviderType
|
||||
IN_MEMORY,
|
||||
TABLE_BASED,
|
||||
REDIS,
|
||||
SHARED_REDIS,
|
||||
CUSTOM
|
||||
}
|
||||
@@ -30,7 +30,6 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
public class QSessionStoreQBitConfig implements QBitConfig
|
||||
{
|
||||
private QSessionStoreProviderType providerType;
|
||||
private SharedRedisSessionStoreSettings sharedRedis;
|
||||
private Duration defaultTtl = Duration.ofHours(1);
|
||||
private Boolean enableSlidingExpiration = true;
|
||||
private String backendName;
|
||||
@@ -78,18 +77,6 @@ public class QSessionStoreQBitConfig implements QBitConfig
|
||||
}
|
||||
}
|
||||
|
||||
if(providerType == QSessionStoreProviderType.SHARED_REDIS)
|
||||
{
|
||||
if(sharedRedis == null)
|
||||
{
|
||||
errors.add("sharedRedis settings are required for SHARED_REDIS provider");
|
||||
}
|
||||
if(defaultTtl == null || defaultTtl.isNegative() || defaultTtl.compareTo(Duration.ofMillis(1)) < 0 || defaultTtl.compareTo(Duration.ofDays(365)) > 0)
|
||||
{
|
||||
errors.add("Shared Redis default TTL must be between one millisecond and 365 days");
|
||||
}
|
||||
}
|
||||
|
||||
if(providerType == QSessionStoreProviderType.CUSTOM)
|
||||
{
|
||||
if(customProviderCodeReference == null)
|
||||
@@ -547,23 +534,4 @@ public class QSessionStoreQBitConfig implements QBitConfig
|
||||
return (this);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** Explicit connection and namespace for the shared Redis provider.
|
||||
***************************************************************************/
|
||||
public SharedRedisSessionStoreSettings getSharedRedis()
|
||||
{
|
||||
return sharedRedis;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Select shared infrastructure without retaining Redis credentials here.
|
||||
***************************************************************************/
|
||||
public QSessionStoreQBitConfig withSharedRedis(SharedRedisSessionStoreSettings settings)
|
||||
{
|
||||
sharedRedis = settings;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,144 +16,63 @@
|
||||
package com.kingsrook.qbits.sessionstore;
|
||||
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Application-scoped configuration and provider access for session-store actions.
|
||||
** Static context holder for the Session Store QBit configuration and provider.
|
||||
*******************************************************************************/
|
||||
public class QSessionStoreQBitContext
|
||||
{
|
||||
private static final Map<QInstance, QSessionStoreQBitConfig> CONFIGS = new WeakHashMap<>();
|
||||
private static final Map<QInstance, QSessionStoreProviderInterface> PROVIDERS = new WeakHashMap<>();
|
||||
private static QSessionStoreQBitConfig config;
|
||||
private static QSessionStoreProviderInterface provider;
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Get configuration for the current application.
|
||||
** Getter for config
|
||||
***************************************************************************/
|
||||
public static QSessionStoreQBitConfig getConfig()
|
||||
{
|
||||
return getConfig(QContext.getQInstance());
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Get configuration for an explicitly selected application.
|
||||
***************************************************************************/
|
||||
public static synchronized QSessionStoreQBitConfig getConfig(QInstance instance)
|
||||
{
|
||||
return CONFIGS.get(instance);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Set configuration for the current application.
|
||||
** Setter for config
|
||||
***************************************************************************/
|
||||
public static void setConfig(QSessionStoreQBitConfig config)
|
||||
{
|
||||
setConfig(QContext.getQInstance(), config);
|
||||
QSessionStoreQBitContext.config = config;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Register configuration during application metadata production.
|
||||
***************************************************************************/
|
||||
public static synchronized void setConfig(QInstance instance, QSessionStoreQBitConfig config)
|
||||
{
|
||||
if(config == null)
|
||||
{
|
||||
CONFIGS.remove(instance);
|
||||
}
|
||||
else
|
||||
{
|
||||
CONFIGS.put(instance, config);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Get the current application's provider; never use another application's.
|
||||
** Getter for provider
|
||||
***************************************************************************/
|
||||
public static QSessionStoreProviderInterface getProvider()
|
||||
{
|
||||
return getProvider(QContext.getQInstance());
|
||||
return provider;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Get an application's provider explicitly without changing thread context.
|
||||
***************************************************************************/
|
||||
public static synchronized QSessionStoreProviderInterface getProvider(QInstance instance)
|
||||
{
|
||||
return PROVIDERS.get(instance);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Set the current application's provider.
|
||||
** Setter for provider
|
||||
***************************************************************************/
|
||||
public static void setProvider(QSessionStoreProviderInterface provider)
|
||||
{
|
||||
setProvider(QContext.getQInstance(), provider);
|
||||
QSessionStoreQBitContext.provider = provider;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Register a provider for its owning application during metadata production.
|
||||
***************************************************************************/
|
||||
public static synchronized void setProvider(QInstance instance, QSessionStoreProviderInterface provider)
|
||||
{
|
||||
if(provider == null)
|
||||
{
|
||||
PROVIDERS.remove(instance);
|
||||
}
|
||||
else
|
||||
{
|
||||
PROVIDERS.put(instance, provider);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Clear only the current application's context.
|
||||
** Clear all context (useful for testing).
|
||||
***************************************************************************/
|
||||
public static void clear()
|
||||
{
|
||||
clear(QContext.getQInstance());
|
||||
config = null;
|
||||
provider = null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Clear only the explicitly selected application's context.
|
||||
***************************************************************************/
|
||||
public static synchronized void clear(QInstance instance)
|
||||
{
|
||||
CONFIGS.remove(instance);
|
||||
PROVIDERS.remove(instance);
|
||||
}
|
||||
/***************************************************************************
|
||||
** Clear shutdown registrations only if they still belong to this provider.
|
||||
***************************************************************************/
|
||||
public static synchronized boolean clearIfSame(QInstance instance, QSessionStoreProviderInterface expected)
|
||||
{
|
||||
if(PROVIDERS.get(instance) != expected)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
clear(instance);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -60,13 +60,6 @@ public class QSessionStoreQBitProducer implements QBitProducer
|
||||
throw new QException("SessionStore QBit configuration errors: " + String.join(", ", errors));
|
||||
}
|
||||
|
||||
if(qInstance.getQBits().values().stream().anyMatch(bit -> GROUP_ID.equals(bit.getGroupId())
|
||||
&& ARTIFACT_ID.equals(bit.getArtifactId())))
|
||||
{
|
||||
throw new QException("SessionStore QBit is already installed on this application");
|
||||
}
|
||||
QSessionStoreProviderInterface provider = QSessionStoreProviderFactory.createProvider(qInstance, config);
|
||||
|
||||
///////////////////////////////
|
||||
// Register QBit identity //
|
||||
///////////////////////////////
|
||||
@@ -82,6 +75,7 @@ public class QSessionStoreQBitProducer implements QBitProducer
|
||||
///////////////////////////////
|
||||
// Create provider instance //
|
||||
///////////////////////////////
|
||||
QSessionStoreProviderInterface provider = QSessionStoreProviderFactory.createProvider(config);
|
||||
LOG.info("Created session store provider", logPair("type", config.getProviderType()));
|
||||
|
||||
///////////////////////////////
|
||||
@@ -96,8 +90,7 @@ public class QSessionStoreQBitProducer implements QBitProducer
|
||||
///////////////////////////////
|
||||
// Produce cleanup process //
|
||||
///////////////////////////////
|
||||
if(Boolean.TRUE.equals(config.getEnableCleanupProcess())
|
||||
&& config.getProviderType() != QSessionStoreProviderType.SHARED_REDIS)
|
||||
if(Boolean.TRUE.equals(config.getEnableCleanupProcess()))
|
||||
{
|
||||
CleanExpiredSessionsProcessMetaDataProducer.produce(qInstance, config);
|
||||
LOG.info("Produced cleanup process");
|
||||
@@ -106,17 +99,13 @@ public class QSessionStoreQBitProducer implements QBitProducer
|
||||
///////////////////////////////
|
||||
// Store in context //
|
||||
///////////////////////////////
|
||||
QSessionStoreQBitContext.setConfig(qInstance, config);
|
||||
QSessionStoreQBitContext.setProvider(qInstance, provider);
|
||||
QSessionStoreQBitContext.setConfig(config);
|
||||
QSessionStoreQBitContext.setProvider(provider);
|
||||
|
||||
///////////////////////////////
|
||||
// Register with core registry //
|
||||
///////////////////////////////
|
||||
QSessionStoreRegistry.getInstance(qInstance).register(provider);
|
||||
if(config.getProviderType() == QSessionStoreProviderType.SHARED_REDIS)
|
||||
{
|
||||
qInstance.withRuntimeService(new com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference(SharedRedisSessionStoreRuntimeService.class));
|
||||
}
|
||||
QSessionStoreRegistry.getInstance().register(provider);
|
||||
LOG.info("Registered session store provider with core registry");
|
||||
}
|
||||
|
||||
|
||||
-147
@@ -1,147 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.kingsrook.qbits.sessionstore;
|
||||
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
import com.kingsrook.qbits.redis.api.RedisAccess;
|
||||
import com.kingsrook.qbits.redis.api.RedisNamespace;
|
||||
import com.kingsrook.qbits.sessionstore.providers.SharedRedisSessionStoreProvider;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.instances.QRuntimeServiceInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.modules.authentication.QSessionStoreRegistry;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Own shared session registrations; Redis alone owns the backing connection pool.
|
||||
*******************************************************************************/
|
||||
public class SharedRedisSessionStoreRuntimeService implements QRuntimeServiceInterface
|
||||
{
|
||||
private static final Map<QInstance, Object> OWNERS = new WeakHashMap<>();
|
||||
private final Object token = new Object();
|
||||
private QInstance instance;
|
||||
private QSessionStoreProviderInterface provider;
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Framework lifecycle identity.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public String getName()
|
||||
{
|
||||
return "shared-redis-session-store";
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Require the named Redis pool first; reconstruct registrations on restart.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public synchronized void start(QInstance owner) throws QException
|
||||
{
|
||||
if(owner == null)
|
||||
{
|
||||
throw new QException("Shared Redis session runtime requires an application");
|
||||
}
|
||||
synchronized(OWNERS)
|
||||
{
|
||||
if(instance != null || OWNERS.containsKey(owner))
|
||||
{
|
||||
throw new QException("Shared Redis session runtime is already started");
|
||||
}
|
||||
OWNERS.put(owner, token);
|
||||
}
|
||||
QSessionStoreProviderInterface candidate = QSessionStoreQBitContext.getProvider(owner);
|
||||
try
|
||||
{
|
||||
if(candidate != null && !(candidate instanceof SharedRedisSessionStoreProvider))
|
||||
{
|
||||
throw new QException("Application has another session provider");
|
||||
}
|
||||
var config = owner.getQBits().values().stream().map(bit -> bit.getConfig())
|
||||
.filter(QSessionStoreQBitConfig.class::isInstance)
|
||||
.map(QSessionStoreQBitConfig.class::cast)
|
||||
.filter(value -> value.getProviderType() == QSessionStoreProviderType.SHARED_REDIS)
|
||||
.findFirst().orElseThrow();
|
||||
var settings = config.getSharedRedis();
|
||||
RedisAccess.cache(owner, settings.connectionName(), new RedisNamespace(settings.deployment(),
|
||||
settings.application(), settings.tenant(), "session-store"), settings.maximumValueBytes());
|
||||
var registered = QSessionStoreRegistry.getInstance(owner).getProvider().orElse(null);
|
||||
if(registered != null && registered != candidate)
|
||||
{
|
||||
throw new QException("Application has another session provider");
|
||||
}
|
||||
if(candidate == null)
|
||||
{
|
||||
candidate = QSessionStoreProviderFactory.createProvider(owner, config);
|
||||
}
|
||||
QSessionStoreQBitContext.setConfig(owner, config);
|
||||
QSessionStoreQBitContext.setProvider(owner, candidate);
|
||||
QSessionStoreRegistry.getInstance(owner).register(candidate);
|
||||
instance = owner;
|
||||
provider = candidate;
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
if(candidate instanceof SharedRedisSessionStoreProvider)
|
||||
{
|
||||
QSessionStoreRegistry.getInstance(owner).clearIfSame(candidate);
|
||||
QSessionStoreQBitContext.clearIfSame(owner, candidate);
|
||||
}
|
||||
synchronized(OWNERS)
|
||||
{
|
||||
OWNERS.remove(owner, token);
|
||||
}
|
||||
throw new QException("Shared Redis session startup failed; check connection startup and provider ownership");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Keep registrations available until dependent HTTP and scheduler work stops.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Release only our registrations, with no QContext requirement or pool closure.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public synchronized void afterApplicationStop()
|
||||
{
|
||||
if(instance == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
QSessionStoreRegistry.getInstance(instance).clearIfSame(provider);
|
||||
QSessionStoreQBitContext.clearIfSame(instance, provider);
|
||||
synchronized(OWNERS)
|
||||
{
|
||||
OWNERS.remove(instance, token);
|
||||
}
|
||||
provider = null;
|
||||
instance = null;
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.kingsrook.qbits.sessionstore;
|
||||
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Explicit shared Redis ownership and namespace; credentials stay in Redis runtime.
|
||||
*******************************************************************************/
|
||||
public record SharedRedisSessionStoreSettings(String connectionName, String deployment, String application, String tenant, Integer maximumValueBytes)
|
||||
{
|
||||
/***************************************************************************
|
||||
** Bound namespace inputs and serialized session payloads before registration.
|
||||
***************************************************************************/
|
||||
public SharedRedisSessionStoreSettings
|
||||
{
|
||||
for(String value : new String[] {connectionName, deployment, application, tenant})
|
||||
{
|
||||
if(value == null || value.isBlank() || value.getBytes(StandardCharsets.UTF_8).length > 256)
|
||||
{
|
||||
throw new IllegalArgumentException("Shared Redis connection and scope must be nonblank and at most 256 bytes");
|
||||
}
|
||||
}
|
||||
if(maximumValueBytes == null || maximumValueBytes < 1 || maximumValueBytes > 1048576)
|
||||
{
|
||||
throw new IllegalArgumentException("Shared Redis session size must be between 1 and 1048576 bytes");
|
||||
}
|
||||
}
|
||||
}
|
||||
-234
@@ -1,234 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.kingsrook.qbits.sessionstore.providers;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qbits.redis.api.RedisAccess;
|
||||
import com.kingsrook.qbits.redis.api.RedisCache;
|
||||
import com.kingsrook.qbits.redis.api.RedisNamespace;
|
||||
import com.kingsrook.qbits.sessionstore.QSessionStoreProviderInterface;
|
||||
import com.kingsrook.qbits.sessionstore.QSessionStoreQBitConfig;
|
||||
import com.kingsrook.qbits.sessionstore.SharedRedisSessionStoreSettings;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Session rules over application-owned Redis infrastructure. Never owns a pool.
|
||||
*******************************************************************************/
|
||||
public final class SharedRedisSessionStoreProvider implements QSessionStoreProviderInterface
|
||||
{
|
||||
private final WeakReference<QInstance> instance;
|
||||
private SharedRedisSessionStoreSettings settings;
|
||||
private RedisNamespace namespace;
|
||||
private Duration defaultTtl;
|
||||
private boolean sliding;
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Bind to the explicit owner, independent of the request thread's context.
|
||||
***************************************************************************/
|
||||
public SharedRedisSessionStoreProvider(QInstance instance)
|
||||
{
|
||||
this.instance = new WeakReference<>(Objects.requireNonNull(instance, "Shared Redis requires an owning QInstance"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Do not retain an abandoned application through a static registry value.
|
||||
***************************************************************************/
|
||||
private QInstance owner()
|
||||
{
|
||||
QInstance owner = instance.get();
|
||||
if(owner == null)
|
||||
{
|
||||
throw new IllegalStateException("Owning application is no longer available");
|
||||
}
|
||||
return owner;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Validate metadata without creating infrastructure; install Redis first.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public void configure(QSessionStoreQBitConfig config)
|
||||
{
|
||||
if(settings != null)
|
||||
{
|
||||
throw new IllegalStateException("Shared Redis session provider is already configured");
|
||||
}
|
||||
var configured = Objects.requireNonNull(config.getSharedRedis());
|
||||
Duration ttl = validTtl(config.getDefaultTtl());
|
||||
if(!RedisAccess.isConfigured(owner(), configured.connectionName()))
|
||||
{
|
||||
throw new IllegalArgumentException("Shared Redis session connection is not configured on the owning application");
|
||||
}
|
||||
namespace = new RedisNamespace(configured.deployment(), configured.application(), configured.tenant(), "session-store");
|
||||
defaultTtl = ttl;
|
||||
sliding = Boolean.TRUE.equals(config.getEnableSlidingExpiration());
|
||||
settings = configured;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Resolve only started owner infrastructure; never allocate a fallback pool.
|
||||
***************************************************************************/
|
||||
private RedisCache cache()
|
||||
{
|
||||
if(settings == null)
|
||||
{
|
||||
throw new IllegalStateException("Shared Redis session provider is not configured");
|
||||
}
|
||||
return RedisAccess.cache(owner(), settings.connectionName(), namespace, settings.maximumValueBytes());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Serialize within session-store and atomically set value plus expiry.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public void store(String id, QSession session, Duration ttl)
|
||||
{
|
||||
cache().put(id, JsonUtils.toJson(Objects.requireNonNull(session)).getBytes(StandardCharsets.UTF_8), validTtl(ttl == null ? defaultTtl : ttl));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Absence is a cache miss; infrastructure and decoding errors propagate.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public Optional<QSession> load(String id)
|
||||
{
|
||||
return decode(cache().get(id));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Failed eviction must propagate so logout can retry rather than claim success.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public void remove(String id)
|
||||
{
|
||||
cache().remove(id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Explicit renewal never recreates an expired or deleted session.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public void touch(String id)
|
||||
{
|
||||
cache().touch(id, defaultTtl);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Respect sliding-expiration configuration with one atomic read/renew command.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public Optional<QSession> loadAndTouch(String id)
|
||||
{
|
||||
return decode(sliding ? cache().getAndTouch(id, defaultTtl) : cache().get(id));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Bound decoded payloads and never include session contents in error messages.
|
||||
***************************************************************************/
|
||||
private Optional<QSession> decode(Optional<byte[]> value)
|
||||
{
|
||||
return value.map(bytes ->
|
||||
{
|
||||
if(bytes.length > settings.maximumValueBytes())
|
||||
{
|
||||
throw new IllegalStateException("Stored session exceeds its configured size limit");
|
||||
}
|
||||
try
|
||||
{
|
||||
return Objects.requireNonNull(JsonUtils.toObject(new String(bytes, StandardCharsets.UTF_8), QSession.class));
|
||||
}
|
||||
catch(IOException | RuntimeException e)
|
||||
{
|
||||
throw new IllegalStateException("Stored session could not be decoded");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Bound TTL arithmetic and prevent immortal or truncated sub-millisecond data.
|
||||
***************************************************************************/
|
||||
private static Duration validTtl(Duration ttl)
|
||||
{
|
||||
if(ttl == null || ttl.compareTo(Duration.ofMillis(1)) < 0 || ttl.compareTo(Duration.ofDays(365)) > 0)
|
||||
{
|
||||
throw new IllegalArgumentException("Session TTL must be between one millisecond and 365 days");
|
||||
}
|
||||
return ttl;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Report the configured session lifetime.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public Duration getDefaultTtl()
|
||||
{
|
||||
return defaultTtl;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Redis owns expiration; no scans or wildcard deletes are needed.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public void cleanExpired()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Health truth belongs to the shared runtime's framework health checks.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public String status()
|
||||
{
|
||||
return "Shared Redis: health is reported by the Redis runtime";
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.kingsrook.qbits.sessionstore;
|
||||
|
||||
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Prove memory consumers can start without either optional Redis artifact.
|
||||
*******************************************************************************/
|
||||
class OptionalRedisSessionConsumerTest
|
||||
{
|
||||
/***************************************************************************
|
||||
** Platform parent prevents optional classes leaking from the Maven runner.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void memoryConsumerWithoutRedis() throws Exception
|
||||
{
|
||||
String classpath = System.getProperty("surefire.test.class.path", System.getProperty("java.class.path"));
|
||||
URL[] urls = Arrays.stream(classpath.split(java.io.File.pathSeparator))
|
||||
.filter(entry -> !entry.replace('\\', '/').contains("/qbit-redis/")
|
||||
&& !Path.of(entry).getFileName().toString().startsWith("jedis-"))
|
||||
.map(entry ->
|
||||
{
|
||||
try
|
||||
{
|
||||
return Path.of(entry).toUri().toURL();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}).toArray(URL[]::new);
|
||||
ClassLoader previous = Thread.currentThread().getContextClassLoader();
|
||||
try(var loader = new URLClassLoader(urls, ClassLoader.getPlatformClassLoader()))
|
||||
{
|
||||
assertThrows(ClassNotFoundException.class,
|
||||
() -> loader.loadClass("com.kingsrook.qbits.redis.api.RedisAccess"));
|
||||
assertThrows(ClassNotFoundException.class,
|
||||
() -> loader.loadClass("redis.clients.jedis.RedisClient"));
|
||||
Thread.currentThread().setContextClassLoader(loader);
|
||||
loader.loadClass(Probe.class.getName()).getMethod("exercise").invoke(null);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Thread.currentThread().setContextClassLoader(previous);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Loaded with session-store and core but without Redis classes.
|
||||
***************************************************************************/
|
||||
public static class Probe
|
||||
{
|
||||
/************************************************************************
|
||||
** Producer, registry and in-memory storage remain usable independently.
|
||||
************************************************************************/
|
||||
public static void exercise() throws Exception
|
||||
{
|
||||
var instance = new com.kingsrook.qqq.backend.core.model.metadata.QInstance();
|
||||
try
|
||||
{
|
||||
new QSessionStoreQBitProducer().withConfig(new QSessionStoreQBitConfig()
|
||||
.withProviderType(QSessionStoreProviderType.IN_MEMORY).withEnableCleanupProcess(false))
|
||||
.produce(instance, "session-store");
|
||||
var provider = QSessionStoreQBitContext.getProvider(instance);
|
||||
provider.store("id", new com.kingsrook.qqq.backend.core.model.session.QSession()
|
||||
.withValue("marker", "memory"), java.time.Duration.ofSeconds(30));
|
||||
assertEquals("memory", provider.load("id").orElseThrow().getValue("marker"));
|
||||
provider.remove("id");
|
||||
assertTrue(provider.load("id").isEmpty());
|
||||
}
|
||||
finally
|
||||
{
|
||||
QSessionStoreQBitContext.clear(instance);
|
||||
com.kingsrook.qqq.backend.core.modules.authentication.QSessionStoreRegistry.getInstance(instance).clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@ package com.kingsrook.qbits.sessionstore;
|
||||
|
||||
import java.time.Duration;
|
||||
import com.kingsrook.qbits.sessionstore.providers.InMemorySessionStoreProvider;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.modules.authentication.QSessionStoreRegistry;
|
||||
@@ -64,19 +63,14 @@ class QSessionStoreQBitProducerTest
|
||||
assertThat(qInstance.getQBits()).hasSize(1);
|
||||
|
||||
// Verify provider was created and stored in context
|
||||
assertThat(QSessionStoreQBitContext.getProvider(qInstance)).isInstanceOf(InMemorySessionStoreProvider.class);
|
||||
assertThat(QSessionStoreQBitContext.getConfig(qInstance)).isSameAs(config);
|
||||
assertThat(QSessionStoreQBitContext.getProvider()).isInstanceOf(InMemorySessionStoreProvider.class);
|
||||
assertThat(QSessionStoreQBitContext.getConfig()).isSameAs(config);
|
||||
|
||||
// Verify cleanup process was created
|
||||
assertThat(qInstance.getProcess("cleanExpiredSessions")).isNotNull();
|
||||
|
||||
// Verify registered with core registry
|
||||
assertThat(QSessionStoreRegistry.getInstance(qInstance).getProvider()).isPresent();
|
||||
var originalProvider = QSessionStoreQBitContext.getProvider(qInstance);
|
||||
assertThatThrownBy(() -> new QSessionStoreQBitProducer().withConfig(config).produce(qInstance, "duplicate"))
|
||||
.isInstanceOf(QException.class).hasMessageContaining("already installed");
|
||||
assertThat(QSessionStoreQBitContext.getProvider(qInstance)).isSameAs(originalProvider);
|
||||
assertThat(qInstance.getQBits()).hasSize(1);
|
||||
assertThat(QSessionStoreRegistry.getInstance().getProvider()).isPresent();
|
||||
}
|
||||
|
||||
|
||||
@@ -149,41 +143,4 @@ class QSessionStoreQBitProducerTest
|
||||
assertThat(QSessionStoreQBitProducer.VERSION).isEqualTo("0.1.0");
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** Metadata production must target the supplied instance despite caller context.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void isolatesApplicationProviders() throws QException
|
||||
{
|
||||
QInstance original = QContext.getQInstance();
|
||||
QInstance first = new QInstance();
|
||||
QInstance second = new QInstance();
|
||||
var firstConfig = new QSessionStoreQBitConfig().withProviderType(QSessionStoreProviderType.IN_MEMORY);
|
||||
var secondConfig = new QSessionStoreQBitConfig().withProviderType(QSessionStoreProviderType.IN_MEMORY);
|
||||
try
|
||||
{
|
||||
QContext.setQInstance(second);
|
||||
new QSessionStoreQBitProducer().withConfig(firstConfig).produce(first, "first");
|
||||
assertThat(QSessionStoreQBitContext.getProvider()).isNull();
|
||||
assertThat(QSessionStoreRegistry.getInstance().getProvider()).isEmpty();
|
||||
new QSessionStoreQBitProducer().withConfig(secondConfig).produce(second, "second");
|
||||
var secondProvider = QSessionStoreQBitContext.getProvider();
|
||||
assertThat(QSessionStoreQBitContext.getConfig()).isSameAs(secondConfig);
|
||||
QContext.setQInstance(first);
|
||||
assertThat(QSessionStoreQBitContext.getConfig()).isSameAs(firstConfig);
|
||||
assertThat(QSessionStoreQBitContext.getProvider()).isNotSameAs(secondProvider);
|
||||
assertThat(QSessionStoreRegistry.getInstance().getProvider()).contains(QSessionStoreQBitContext.getProvider());
|
||||
QSessionStoreQBitContext.clear();
|
||||
QContext.setQInstance(second);
|
||||
assertThat(QSessionStoreQBitContext.getProvider()).isSameAs(secondProvider);
|
||||
}
|
||||
finally
|
||||
{
|
||||
QSessionStoreQBitContext.clear(first);
|
||||
QSessionStoreQBitContext.clear(second);
|
||||
QSessionStoreRegistry.getInstance(first).clear();
|
||||
QSessionStoreRegistry.getInstance(second).clear();
|
||||
QContext.setQInstance(original);
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
-5
@@ -27,33 +27,57 @@ import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.DockerClientFactory;
|
||||
import org.testcontainers.containers.GenericContainer;
|
||||
import org.testcontainers.utility.DockerImageName;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assumptions.assumeTrue;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Integration tests for RedisSessionStoreProvider using Testcontainers.
|
||||
** These tests require Docker; unavailable infrastructure fails verification.
|
||||
** These tests require Docker to be running. If Docker is not available,
|
||||
** the tests will be skipped.
|
||||
*******************************************************************************/
|
||||
class RedisSessionStoreProviderIT
|
||||
{
|
||||
private static final int REDIS_PORT = 6379;
|
||||
|
||||
private static boolean dockerAvailable = false;
|
||||
private static GenericContainer<?> redis;
|
||||
private RedisSessionStoreProvider provider;
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Start required Redis infrastructure; failures must fail verification.
|
||||
** Check Docker availability and start container if available.
|
||||
***************************************************************************/
|
||||
@BeforeAll
|
||||
static void startContainer()
|
||||
{
|
||||
redis = new GenericContainer<>(DockerImageName.parse("redis:7.4-alpine"))
|
||||
.withExposedPorts(REDIS_PORT);
|
||||
redis.start();
|
||||
try
|
||||
{
|
||||
dockerAvailable = DockerClientFactory.instance().isDockerAvailable();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
dockerAvailable = false;
|
||||
}
|
||||
|
||||
if(dockerAvailable)
|
||||
{
|
||||
try
|
||||
{
|
||||
redis = new GenericContainer<>(DockerImageName.parse("redis:7-alpine"))
|
||||
.withExposedPorts(REDIS_PORT);
|
||||
redis.start();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
dockerAvailable = false;
|
||||
redis = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,6 +99,9 @@ class RedisSessionStoreProviderIT
|
||||
@BeforeEach
|
||||
void setUp()
|
||||
{
|
||||
assumeTrue(dockerAvailable && redis != null && redis.isRunning(),
|
||||
"Docker is not available or Redis container is not running - skipping test");
|
||||
|
||||
provider = new RedisSessionStoreProvider();
|
||||
|
||||
QSessionStoreQBitConfig config = new QSessionStoreQBitConfig()
|
||||
|
||||
-440
@@ -1,440 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.kingsrook.qbits.sessionstore.providers;
|
||||
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.redis.RedisConnectionConfig;
|
||||
import com.kingsrook.qbits.redis.RedisQBitConfig;
|
||||
import com.kingsrook.qbits.redis.RedisQBitProducer;
|
||||
import com.kingsrook.qbits.redis.RedisRuntimeService;
|
||||
import com.kingsrook.qbits.redis.api.RedisAccess;
|
||||
import com.kingsrook.qbits.redis.api.RedisNamespace;
|
||||
import com.kingsrook.qbits.sessionstore.QSessionStoreProviderType;
|
||||
import com.kingsrook.qbits.sessionstore.QSessionStoreQBitConfig;
|
||||
import com.kingsrook.qbits.sessionstore.QSessionStoreQBitContext;
|
||||
import com.kingsrook.qbits.sessionstore.QSessionStoreQBitProducer;
|
||||
import com.kingsrook.qbits.sessionstore.SharedRedisSessionStoreRuntimeService;
|
||||
import com.kingsrook.qbits.sessionstore.SharedRedisSessionStoreSettings;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.modules.authentication.QSessionStoreRegistry;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.containers.GenericContainer;
|
||||
import org.testcontainers.utility.DockerImageName;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Real shared infrastructure proves namespace, lifecycle and expiry ownership.
|
||||
*******************************************************************************/
|
||||
class SharedRedisSessionStoreProviderIT
|
||||
{
|
||||
/***************************************************************************
|
||||
** Separate nodes share one app's sessions while other apps remain isolated.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void sharedPoolsIsolationExpiryAndFailure() throws Exception
|
||||
{
|
||||
QInstance original = QContext.getQInstance();
|
||||
try(GenericContainer<?> redis = new GenericContainer<>(DockerImageName.parse("redis:7.4-alpine")).withExposedPorts(6379))
|
||||
{
|
||||
redis.start();
|
||||
QInstance first = host(redis, "app-a", true);
|
||||
QInstance second = host(redis, "app-a", false);
|
||||
QInstance other = host(redis, "app-b", true);
|
||||
var a = QSessionStoreQBitContext.getProvider(first);
|
||||
var b = QSessionStoreQBitContext.getProvider(second);
|
||||
var c = QSessionStoreQBitContext.getProvider(other);
|
||||
var firstRuntime = new RedisRuntimeService();
|
||||
var secondRuntime = new RedisRuntimeService();
|
||||
var otherRuntime = new RedisRuntimeService();
|
||||
try
|
||||
{
|
||||
assertThrows(IllegalStateException.class, () -> a.load("session"));
|
||||
firstRuntime.start(first);
|
||||
secondRuntime.start(second);
|
||||
otherRuntime.start(other);
|
||||
QContext.setQInstance(other);
|
||||
a.store("session", new QSession().withValue("owner", "app-a"), null);
|
||||
assertEquals("app-a", b.load("session").orElseThrow().getValue("owner"));
|
||||
assertTrue(c.load("session").isEmpty());
|
||||
c.store("session", new QSession().withValue("owner", "app-b"), null);
|
||||
a.remove("session");
|
||||
assertTrue(b.load("session").isEmpty());
|
||||
assertEquals("app-b", c.load("session").orElseThrow().getValue("owner"));
|
||||
a.touch("session");
|
||||
assertTrue(a.loadAndTouch("session").isEmpty());
|
||||
|
||||
var scope = new RedisNamespace("test", "app-a", "authentication", "session-store");
|
||||
var client = RedisRuntimeService.connection(first, "sessions").client();
|
||||
a.store("sliding", new QSession(), Duration.ofSeconds(1));
|
||||
assertTrue(a.loadAndTouch("sliding").isPresent());
|
||||
assertTrue(client.pttl(scope.key("sliding")) > 20000);
|
||||
a.store("fixed", new QSession(), Duration.ofMillis(200));
|
||||
assertTrue(b.loadAndTouch("fixed").isPresent());
|
||||
assertTrue(client.pttl(scope.key("fixed")) <= 200);
|
||||
long deadline = System.nanoTime() + Duration.ofSeconds(3).toNanos();
|
||||
while(b.load("fixed").isPresent() && System.nanoTime() < deadline)
|
||||
{
|
||||
Thread.sleep(10);
|
||||
}
|
||||
assertTrue(b.load("fixed").isEmpty());
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> a.store("bad", new QSession(), Duration.ZERO));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> a.store("large", new QSession().withValue("large", "x".repeat(70000)), null));
|
||||
RedisAccess.cache(first, "sessions", scope, 65536)
|
||||
.put("broken", "test-secret-not-json".getBytes(java.nio.charset.StandardCharsets.UTF_8), Duration.ofSeconds(5));
|
||||
var malformed = assertThrows(IllegalStateException.class, () -> a.load("broken"));
|
||||
assertFalse(malformed.toString().contains("test-secret"));
|
||||
assertNull(malformed.getCause());
|
||||
a.cleanExpired();
|
||||
assertTrue(a.status().contains("health"));
|
||||
assertEquals(Duration.ofSeconds(30), a.getDefaultTtl());
|
||||
|
||||
firstRuntime.stop();
|
||||
firstRuntime.afterApplicationStop();
|
||||
assertThrows(IllegalStateException.class, () -> a.load("sliding"));
|
||||
assertTrue(b.load("sliding").isPresent());
|
||||
firstRuntime.start(first);
|
||||
assertTrue(a.load("sliding").isPresent());
|
||||
redis.stop();
|
||||
assertThrows(RuntimeException.class, () -> a.remove("sliding"));
|
||||
assertThrows(RuntimeException.class, () -> a.load("sliding"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
for(var runtime : List.of(firstRuntime, secondRuntime, otherRuntime))
|
||||
{
|
||||
runtime.stop();
|
||||
runtime.afterApplicationStop();
|
||||
}
|
||||
for(var instance : List.of(first, second, other))
|
||||
{
|
||||
QSessionStoreQBitContext.clear(instance);
|
||||
QSessionStoreRegistry.getInstance(instance).clear();
|
||||
}
|
||||
QContext.setQInstance(original);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Startup failures and shutdown must release only the owning registrations.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void registrationLifecycle() throws Exception
|
||||
{
|
||||
QInstance original = QContext.getQInstance();
|
||||
try(GenericContainer<?> redis = new GenericContainer<>(DockerImageName.parse("redis:7.4-alpine")).withExposedPorts(6379))
|
||||
{
|
||||
redis.start();
|
||||
QInstance first = host(redis, "app-a", true);
|
||||
QInstance second = host(redis, "app-b", true);
|
||||
var firstPool = new RedisRuntimeService();
|
||||
var secondPool = new RedisRuntimeService();
|
||||
var firstService = new SharedRedisSessionStoreRuntimeService();
|
||||
var secondService = new SharedRedisSessionStoreRuntimeService();
|
||||
try
|
||||
{
|
||||
assertThrows(com.kingsrook.qqq.backend.core.exceptions.QException.class,
|
||||
() -> firstService.start(first));
|
||||
assertNull(QSessionStoreQBitContext.getProvider(first));
|
||||
assertTrue(QSessionStoreRegistry.getInstance(first).getProvider().isEmpty());
|
||||
firstService.afterApplicationStop();
|
||||
firstPool.start(first);
|
||||
secondPool.start(second);
|
||||
firstService.start(first);
|
||||
secondService.start(second);
|
||||
var firstProvider = QSessionStoreQBitContext.getProvider(first);
|
||||
var secondProvider = QSessionStoreQBitContext.getProvider(second);
|
||||
assertNotNull(firstProvider);
|
||||
assertThrows(com.kingsrook.qqq.backend.core.exceptions.QException.class,
|
||||
() -> firstService.start(first));
|
||||
var duplicate = new SharedRedisSessionStoreRuntimeService();
|
||||
assertThrows(com.kingsrook.qqq.backend.core.exceptions.QException.class,
|
||||
() -> duplicate.start(first));
|
||||
duplicate.afterApplicationStop();
|
||||
assertSame(firstProvider, QSessionStoreQBitContext.getProvider(first));
|
||||
QContext.setQInstance(second);
|
||||
firstService.stop();
|
||||
assertSame(firstProvider, QSessionStoreQBitContext.getProvider(first));
|
||||
firstService.afterApplicationStop();
|
||||
firstService.afterApplicationStop();
|
||||
assertNull(QSessionStoreQBitContext.getProvider(first));
|
||||
assertNull(QSessionStoreQBitContext.getConfig(first));
|
||||
assertTrue(QSessionStoreRegistry.getInstance(first).getProvider().isEmpty());
|
||||
assertSame(secondProvider, QSessionStoreQBitContext.getProvider(second));
|
||||
assertEquals("PONG", RedisRuntimeService.connection(first, "sessions").client().ping());
|
||||
firstService.start(first);
|
||||
assertNotSame(firstProvider, QSessionStoreQBitContext.getProvider(first));
|
||||
var replacement = new InMemorySessionStoreProvider();
|
||||
QSessionStoreRegistry.getInstance(first).register(replacement);
|
||||
QSessionStoreQBitContext.setProvider(first, replacement);
|
||||
firstService.afterApplicationStop();
|
||||
assertSame(replacement, QSessionStoreQBitContext.getProvider(first));
|
||||
assertSame(replacement, QSessionStoreRegistry.getInstance(first).getProvider().orElseThrow());
|
||||
assertThrows(com.kingsrook.qqq.backend.core.exceptions.QException.class,
|
||||
() -> firstService.start(first));
|
||||
assertSame(replacement, QSessionStoreQBitContext.getProvider(first));
|
||||
assertSame(replacement, QSessionStoreRegistry.getInstance(first).getProvider().orElseThrow());
|
||||
}
|
||||
finally
|
||||
{
|
||||
firstService.afterApplicationStop();
|
||||
secondService.afterApplicationStop();
|
||||
firstPool.afterApplicationStop();
|
||||
secondPool.afterApplicationStop();
|
||||
for(var instance : List.of(first, second))
|
||||
{
|
||||
QSessionStoreQBitContext.clear(instance);
|
||||
QSessionStoreRegistry.getInstance(instance).clear();
|
||||
}
|
||||
QContext.setQInstance(original);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Exercise real launcher callbacks and rollback after a later service fails.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void applicationLauncherOwnsNormalAndFailedShutdown() throws Exception
|
||||
{
|
||||
var original = QContext.capture();
|
||||
try(GenericContainer<?> redis = new GenericContainer<>(DockerImageName.parse("redis:7.4-alpine")).withExposedPorts(6379))
|
||||
{
|
||||
redis.start();
|
||||
QInstance instance = host(redis, "launcher", true);
|
||||
var http = new java.util.concurrent.atomic.AtomicReference<org.eclipse.jetty.server.Server>();
|
||||
var launcher = com.kingsrook.qqq.middleware.javalin.QApplicationLauncher.run(
|
||||
application(instance), launcherConfig(http));
|
||||
var pool = RedisRuntimeService.connection(instance, "sessions");
|
||||
try
|
||||
{
|
||||
assertTrue(http.get().isStarted());
|
||||
assertTrue(launcher.getStartedServiceNames().containsAll(List.of("redis", "shared-redis-session-store")));
|
||||
var provider = QSessionStoreQBitContext.getProvider(instance);
|
||||
provider.store("launcher-session", new QSession().withValue("owner", "launcher"), null);
|
||||
assertEquals("launcher", provider.load("launcher-session").orElseThrow().getValue("owner"));
|
||||
QContext.clear();
|
||||
}
|
||||
finally
|
||||
{
|
||||
launcher.stop();
|
||||
}
|
||||
launcher.stop();
|
||||
assertTrue(http.get().isStopped());
|
||||
assertNull(QSessionStoreQBitContext.getProvider(instance));
|
||||
assertNull(QSessionStoreQBitContext.getConfig(instance));
|
||||
assertTrue(QSessionStoreRegistry.getInstance(instance).getProvider().isEmpty());
|
||||
assertThrows(IllegalStateException.class, () -> RedisRuntimeService.connection(instance, "sessions"));
|
||||
assertThrows(RuntimeException.class, () -> pool.client().ping());
|
||||
|
||||
QInstance failing = host(redis, "failed-launcher", true);
|
||||
failing.withRuntimeService(new com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference(FailAfterSessions.class));
|
||||
var failedHttp = new java.util.concurrent.atomic.AtomicReference<org.eclipse.jetty.server.Server>();
|
||||
var failure = assertThrows(com.kingsrook.qqq.backend.core.exceptions.QException.class,
|
||||
() -> com.kingsrook.qqq.middleware.javalin.QApplicationLauncher.run(application(failing), launcherConfig(failedHttp)));
|
||||
assertTrue(failure.getMessage().contains("owned late failure"));
|
||||
assertNotNull(failedHttp.get());
|
||||
assertTrue(failedHttp.get().isStopped());
|
||||
assertNull(QSessionStoreQBitContext.getProvider(failing));
|
||||
assertNull(QSessionStoreQBitContext.getConfig(failing));
|
||||
assertTrue(QSessionStoreRegistry.getInstance(failing).getProvider().isEmpty());
|
||||
assertThrows(IllegalStateException.class, () -> RedisRuntimeService.connection(failing, "sessions"));
|
||||
|
||||
redis.execInContainer("redis-cli", "CONFIG", "SET", "requirepass", "owned-startup-password");
|
||||
QInstance early = host(redis, "early-failure", true);
|
||||
var configuredProvider = QSessionStoreQBitContext.getProvider(early);
|
||||
var earlyHttp = new java.util.concurrent.atomic.AtomicReference<org.eclipse.jetty.server.Server>();
|
||||
var earlyFailure = assertThrows(com.kingsrook.qqq.backend.core.exceptions.QException.class,
|
||||
() -> com.kingsrook.qqq.middleware.javalin.QApplicationLauncher.run(application(early), launcherConfig(earlyHttp)));
|
||||
assertTrue(earlyFailure.getMessage().contains("Redis startup failed"));
|
||||
assertTrue(earlyHttp.get().isStopped());
|
||||
assertThrows(IllegalStateException.class, () -> RedisRuntimeService.connection(early, "sessions"));
|
||||
assertSame(configuredProvider, QSessionStoreQBitContext.getProvider(early));
|
||||
assertThrows(IllegalStateException.class, () -> configuredProvider.load("unstarted"));
|
||||
assertEquals(0, redis.execInContainer("redis-cli", "-a", "owned-startup-password",
|
||||
"CONFIG", "SET", "requirepass", "").getExitCode());
|
||||
var retriedHttp = new java.util.concurrent.atomic.AtomicReference<org.eclipse.jetty.server.Server>();
|
||||
var retried = com.kingsrook.qqq.middleware.javalin.QApplicationLauncher.run(application(early), launcherConfig(retriedHttp));
|
||||
try
|
||||
{
|
||||
assertTrue(retriedHttp.get().isStarted());
|
||||
configuredProvider.store("retry", new QSession(), null);
|
||||
assertTrue(configuredProvider.load("retry").isPresent());
|
||||
}
|
||||
finally
|
||||
{
|
||||
retried.stop();
|
||||
}
|
||||
assertTrue(retriedHttp.get().isStopped());
|
||||
assertNull(QSessionStoreQBitContext.getProvider(early));
|
||||
assertTrue(QSessionStoreRegistry.getInstance(early).getProvider().isEmpty());
|
||||
assertThrows(IllegalStateException.class, () -> RedisRuntimeService.connection(early, "sessions"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
QContext.clear();
|
||||
QContext.init(original);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The launcher receives the same already-produced metadata as the runtime tests.
|
||||
***************************************************************************/
|
||||
private com.kingsrook.qqq.backend.core.instances.AbstractQQQApplication application(QInstance instance)
|
||||
{
|
||||
return new com.kingsrook.qqq.backend.core.instances.AbstractQQQApplication()
|
||||
{
|
||||
/**********************************************************************
|
||||
** Return this test's owned application, never a global fixture.
|
||||
**********************************************************************/
|
||||
@Override
|
||||
public QInstance defineQInstance()
|
||||
{
|
||||
return instance;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Bind an ephemeral loopback HTTP port and observe the real Jetty lifecycle.
|
||||
***************************************************************************/
|
||||
private com.kingsrook.qqq.middleware.javalin.QApplicationLauncherConfig launcherConfig(
|
||||
java.util.concurrent.atomic.AtomicReference<org.eclipse.jetty.server.Server> http)
|
||||
{
|
||||
return new com.kingsrook.qqq.middleware.javalin.QApplicationLauncherConfig()
|
||||
.withRegisterShutdownHook(false).withServerCustomizer(server -> server.withPort(0)
|
||||
.withServeFrontendNext(false).withServeFrontendMaterialDashboard(false)
|
||||
.withJavalinConfigCustomizer(config ->
|
||||
{
|
||||
config.jetty.host = "127.0.0.1";
|
||||
config.jetty.modifyServer(http::set);
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Fail only after proving Redis and session registration have started.
|
||||
***************************************************************************/
|
||||
public static class FailAfterSessions implements com.kingsrook.qqq.backend.core.instances.QRuntimeServiceInterface
|
||||
{
|
||||
/***********************************************************************
|
||||
** Stable failure-stage name for launcher diagnostics.
|
||||
***********************************************************************/
|
||||
@Override
|
||||
public String getName()
|
||||
{
|
||||
return "owned-late-failure";
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***********************************************************************
|
||||
** The thrown message proves the preceding runtime dependencies were ready.
|
||||
***********************************************************************/
|
||||
@Override
|
||||
public void start(QInstance instance) throws com.kingsrook.qqq.backend.core.exceptions.QException
|
||||
{
|
||||
if(QSessionStoreQBitContext.getProvider(instance) == null
|
||||
|| !"PONG".equals(RedisRuntimeService.connection(instance, "sessions").client().ping()))
|
||||
{
|
||||
throw new com.kingsrook.qqq.backend.core.exceptions.QException("Preceding services were not ready");
|
||||
}
|
||||
throw new com.kingsrook.qqq.backend.core.exceptions.QException("owned late failure");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***********************************************************************
|
||||
** No resources are acquired by this deliberately failing service.
|
||||
***********************************************************************/
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Reject bad settings or missing infrastructure before publishing qbit metadata.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void invalidConfigurationDoesNotRegister() throws Exception
|
||||
{
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new SharedRedisSessionStoreSettings("", "test", "app", "auth", 100));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new SharedRedisSessionStoreSettings("pool", "test", "app", "auth", 0));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new SharedRedisSessionStoreSettings("pool", "test", "app", "auth", 1048577));
|
||||
QInstance instance = new QInstance();
|
||||
var config = new QSessionStoreQBitConfig().withProviderType(QSessionStoreProviderType.SHARED_REDIS);
|
||||
assertThrows(com.kingsrook.qqq.backend.core.exceptions.QException.class,
|
||||
() -> new QSessionStoreQBitProducer().withConfig(config).produce(instance, "session-store"));
|
||||
config.withSharedRedis(new SharedRedisSessionStoreSettings("missing", "test", "app", "auth", 65536));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new QSessionStoreQBitProducer().withConfig(config).produce(instance, "session-store"));
|
||||
assertTrue(instance.getQBits().isEmpty());
|
||||
assertTrue(QSessionStoreRegistry.getInstance(instance).getProvider().isEmpty());
|
||||
assertNull(QSessionStoreQBitContext.getProvider(instance));
|
||||
config.withDefaultTtl(Duration.ZERO);
|
||||
assertThrows(com.kingsrook.qqq.backend.core.exceptions.QException.class,
|
||||
() -> new QSessionStoreQBitProducer().withConfig(config).produce(instance, "session-store"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Compose framework metadata and bind the session provider before pool startup.
|
||||
***************************************************************************/
|
||||
private QInstance host(GenericContainer<?> redis, String application, boolean sliding) throws Exception
|
||||
{
|
||||
var instance = new QInstance();
|
||||
instance.addBackend(new com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData()
|
||||
.withName("memory").withBackendType(com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule.class));
|
||||
instance.withInstanceDefaultAuthentication(new com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData()
|
||||
.withName("test").withType(com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType.FULLY_ANONYMOUS));
|
||||
instance.addTable(new com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData().withName("record").withBackendName("memory").withPrimaryKeyField("id")
|
||||
.withField(new com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData("id", com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType.INTEGER)));
|
||||
new RedisQBitProducer().withQBitConfig(new RedisQBitConfig().withConnection("sessions",
|
||||
new RedisConnectionConfig().withHost(redis.getHost()).withPort(redis.getMappedPort(6379))
|
||||
.withTls(false).withSocketTimeoutMillis(300).withConnectionTimeoutMillis(300)))
|
||||
.produce(instance);
|
||||
new QSessionStoreQBitProducer().withConfig(new QSessionStoreQBitConfig()
|
||||
.withProviderType(QSessionStoreProviderType.SHARED_REDIS)
|
||||
.withSharedRedis(new SharedRedisSessionStoreSettings("sessions", "test", application, "authentication", 65536))
|
||||
.withDefaultTtl(Duration.ofSeconds(30)).withEnableSlidingExpiration(sliding))
|
||||
.produce(instance, "session-store");
|
||||
assertNull(instance.getProcess("cleanExpiredSessions"));
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user