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>
119 lines
6.4 KiB
Plaintext
119 lines
6.4 KiB
Plaintext
# 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.
|