docs(keycloak-session-store): import the session-storage lab as a new project
The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.
Follows the import procedure in README.md.
source/ the originating repository verbatim — 78 documents, 28 SVGs,
8 manifests, plus .source-revision recording the commit
final/ the SSOT
document.md 729 lines written from the 29 experiment documents, not
concatenated: what was predicted, what was measured, and
where the measurement itself was wrong
evidence/raw 125 outputs, flattened to <experiment>__<file> because
the originals collided (01-baseline.txt appeared three
times) and the audit only globs the top level
evidence/meta one per raw file; command and exitCode are null and the
README says why rather than inventing them
evidence/browser 22 captures
assets/ three diagrams through techviz
.techviz/ their VizSpecs
A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.
Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.
verify-pipeline.py passes. audit-records.py reports no issues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
43bccd08a8
commit
b2963105a8
+118
@@ -0,0 +1,118 @@
|
||||
# revision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
|
||||
## A. the port declares the bounded overload and says why it exists
|
||||
/**
|
||||
* Deletes at most {@code limit} published rows older than the cutoff.
|
||||
*
|
||||
* <p>The unbounded version deletes everything before the cutoff in one statement. On a table that
|
||||
* has been accumulating published rows since the last sweep that is a single long transaction
|
||||
* holding locks and generating WAL in proportion to the backlog, which shows up as the relay and
|
||||
* the business writes stalling behind retention. The cleanup jobs describe themselves as bounded
|
||||
* by batch size; this is the parameter that makes that true.
|
||||
*
|
||||
* @return how many rows were deleted; fewer than {@code limit} means the sweep is finished
|
||||
*/
|
||||
int purgePublishedBefore(Instant publishedBefore, int limit);
|
||||
|
||||
## B. both JDBC repositories implement it
|
||||
141: public int purgeProcessedBefore(Instant processedBefore, int limit) {
|
||||
486: public int purgePublishedBefore(Instant publishedBefore, int limit) {
|
||||
--- and the inbox implementation really is bounded
|
||||
connection.prepareStatement(
|
||||
"""
|
||||
WITH expired AS (
|
||||
SELECT message_id, consumer_id
|
||||
FROM messaging_inbox
|
||||
WHERE processed_at < ?
|
||||
ORDER BY processed_at
|
||||
LIMIT ?
|
||||
FOR UPDATE SKIP LOCKED
|
||||
)
|
||||
DELETE FROM messaging_inbox i
|
||||
USING expired e
|
||||
WHERE i.message_id = e.message_id AND i.consumer_id = e.consumer_id
|
||||
""")) {
|
||||
|
||||
## C. every appearance of the two-argument form in the repository
|
||||
src/messaging/messaging-inbox-jdbc-postgresql/src/main/java/dev/caskeleton/messaging/inbox/JdbcInboxRepository.java:141: public int purgeProcessedBefore(Instant processedBefore, int limit) {
|
||||
src/messaging/messaging-inbox-jdbc-postgresql/src/test/java/dev/caskeleton/messaging/inbox/IdempotentConsumerTest.java:106: public int purgeProcessedBefore(Instant processedBefore, int limit) {
|
||||
src/messaging/messaging-inbox-jdbc-postgresql/src/test/java/dev/caskeleton/messaging/inbox/InboxOperationsTest.java:46: public int purgeProcessedBefore(Instant processedBefore, int limit) {
|
||||
src/messaging/messaging-outbox-jdbc-postgresql/src/main/java/dev/caskeleton/messaging/outbox/JdbcOutboxRepository.java:486: public int purgePublishedBefore(Instant publishedBefore, int limit) {
|
||||
src/messaging/messaging-outbox-jdbc-postgresql/src/test/java/dev/caskeleton/messaging/outbox/OutboxOperationsTest.java:125: public int purgePublishedBefore(Instant publishedBefore, int limit) {
|
||||
src/messaging/messaging-outbox-jdbc-postgresql/src/test/java/dev/caskeleton/messaging/outbox/OutboxRelayTest.java:404: public int purgePublishedBefore(Instant publishedBefore, int limit) {
|
||||
src/messaging/messaging-reliability-api/src/main/java/dev/caskeleton/messaging/reliability/InboxRepository.java:52: int purgeProcessedBefore(Instant processedBefore, int limit);
|
||||
src/messaging/messaging-reliability-api/src/main/java/dev/caskeleton/messaging/reliability/OutboxRepository.java:151: int purgePublishedBefore(Instant publishedBefore, int limit);
|
||||
src/messaging/messaging-spring-boot-starter/src/test/java/dev/caskeleton/messaging/autoconfigure/MessagingOutboxRelayLifecycleTest.java:199: public int purgePublishedBefore(Instant publishedBefore, int limit) {
|
||||
(every line above is a declaration, a production implementation, or a test fake override.
|
||||
2 port declarations + 2 production implementations + 5 test fake overrides = 9.
|
||||
None of them is a call site.)
|
||||
|
||||
## D. what the two cleanup jobs actually call
|
||||
54- int removed = 0;
|
||||
55- for (int batch = 0; batch < maxBatches; batch++) {
|
||||
56: int deleted = inbox.purgeProcessedBefore(cutoff);
|
||||
57- removed += deleted;
|
||||
58- // A short batch means the backlog is drained; continuing would just re-scan an empty range.
|
||||
48- int removed = 0;
|
||||
49- for (int batch = 0; batch < maxBatches; batch++) {
|
||||
50: int deleted = outbox.purgePublishedBefore(cutoff);
|
||||
51- removed += deleted;
|
||||
52- if (deleted == 0) {
|
||||
|
||||
## E. InboxCleanupJob declares a batch size and never uses it
|
||||
src/messaging/messaging-inbox-jdbc-postgresql/src/main/java/dev/caskeleton/messaging/inbox/InboxCleanupJob.java:21: public static final int DEFAULT_BATCH_SIZE = 1_000;
|
||||
(one line = the declaration only)
|
||||
|
||||
## F. what the unbounded implementation issues
|
||||
@Override
|
||||
public int purgeProcessedBefore(Instant processedBefore) {
|
||||
Objects.requireNonNull(processedBefore, "processedBefore must not be null");
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
PreparedStatement statement =
|
||||
connection.prepareStatement("DELETE FROM messaging_inbox WHERE processed_at < ?")) {
|
||||
statement.setTimestamp(1, Timestamp.from(processedBefore));
|
||||
return statement.executeUpdate();
|
||||
} catch (SQLException exception) {
|
||||
throw new MessagingConfigurationException(
|
||||
"INBOX_PURGE_FAILED", "could not purge the inbox", exception);
|
||||
}
|
||||
}
|
||||
|
||||
## G. the test named for the property, and the fake it runs against
|
||||
@Override
|
||||
public int purgeProcessedBefore(Instant processedBefore, int limit) {
|
||||
return Math.min(purgeProcessedBefore(processedBefore), limit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int purgeProcessedBefore(Instant processedBefore) {
|
||||
cutoffs.add(processedBefore);
|
||||
return pass < deletions.size() ? deletions.get(pass++) : 0;
|
||||
}
|
||||
}
|
||||
@Test
|
||||
void cleanupDeletesInBoundedBatches() {
|
||||
InMemoryInbox inbox = new InMemoryInbox(List.of(1000, 500));
|
||||
|
||||
int removed =
|
||||
new InboxCleanupJob(inbox, policy(Duration.ofDays(7), Duration.ofDays(1)), 10).runOnce(NOW);
|
||||
|
||||
assertThat(removed).isEqualTo(1500);
|
||||
assertThat(inbox.cutoffs).hasSize(3);
|
||||
}
|
||||
|
||||
## H. the container lane calls the unbounded form too
|
||||
@Test
|
||||
void retentionRemovesOldRows() {
|
||||
MessageId purged = MessageId.newId();
|
||||
inTransaction(() -> repository.reserve(purged, "order-projection", NOW));
|
||||
|
||||
assertThat(repository.purgeProcessedBefore(NOW.plusSeconds(1))).isEqualTo(1);
|
||||
}
|
||||
|
||||
## verdict
|
||||
Both ports declare a bounded purge, both JDBC classes implement it with LIMIT,
|
||||
and no code calls it. Both cleanup jobs call the unbounded overload, which the
|
||||
port's own javadoc describes as the single long transaction the bounded one exists
|
||||
to avoid. The test that asserts bounded batching runs against a scripted fake whose
|
||||
unbounded method returns pre-set counts, so the bound is never exercised.
|
||||
Reference in New Issue
Block a user