fix: a rule read that asks for its own rows no longer recurses (release/0.1 port of #43) #44

Merged
james.maes merged 1 commits from fix/reentrant-rule-reload-0.1 into release/0.1 2026-11-14 08:23:09 +00:00
Owner

Port of #43 to the 0.1 release line: the same commit, cherry-picked onto release/0.1 (the two branches differ only in the version in pom.xml). Earlier fixes reached this line by merging develop into it (#40, #42); this one is a pull request of its own so that it can be reviewed and released without waiting for that merge. Merge #43 first.

The defect

With one row in secured_data_classification, and another QBit's virtual field value provider on that table whose work is a query with filter criteria, every request that needed a classification ended in StackOverflowError. Core asks this QBit for a classification when it validates a query's criteria. The provider's query ran inside the read of the classification rows, so its question started that read again, and nothing was ever recorded that would stop the next request doing the same. The active retention rules and the policies were read in the same way.

The fix

  1. System reads of the QBit's own tables name the stored fields of the table, so core runs no virtual field value provider inside them.
  2. A question asked from inside a rule read is answered from the rows held when the read began (none before the first read) and starts no other read (RuleRows, used for classifications, retention rules and policies). This covers code that naming the fields cannot switch off, such as a post-query customizer.
  3. A read that fails records nothing, so the next question reads again.
  4. A transaction's read of its own retention rule edits is guarded in the same way.

Only the reading thread, inside the QBit's system read, can be given the nested answer. There the session is the system session and the resolver answers FULL without looking at a classification, so no field access decision is made from it. Every request, on that thread or another, gets the rows the read returns, or the error. #43 has the full argument and the table of tests.

Tests

SecuredDataRuleReloadTest, 11 tests; 9 of them end in StackOverflowError without the main-code changes.

Build

./mvnw -B verify on this head: 112 tests, 0 failures, Checkstyle and the JaCoCo gates green. Nothing skipped.

🤖 Generated with Claude Code

Port of #43 to the 0.1 release line: the same commit, cherry-picked onto `release/0.1` (the two branches differ only in the version in `pom.xml`). Earlier fixes reached this line by merging `develop` into it (#40, #42); this one is a pull request of its own so that it can be reviewed and released without waiting for that merge. Merge #43 first. ## The defect With one row in `secured_data_classification`, and another QBit's virtual field value provider on that table whose work is a query with filter criteria, every request that needed a classification ended in `StackOverflowError`. Core asks this QBit for a classification when it validates a query's criteria. The provider's query ran inside the read of the classification rows, so its question started that read again, and nothing was ever recorded that would stop the next request doing the same. The active retention rules and the policies were read in the same way. ## The fix 1. System reads of the QBit's own tables name the stored fields of the table, so core runs no virtual field value provider inside them. 2. A question asked from inside a rule read is answered from the rows held when the read began (none before the first read) and starts no other read (`RuleRows`, used for classifications, retention rules and policies). This covers code that naming the fields cannot switch off, such as a post-query customizer. 3. A read that fails records nothing, so the next question reads again. 4. A transaction's read of its own retention rule edits is guarded in the same way. Only the reading thread, inside the QBit's system read, can be given the nested answer. There the session is the system session and the resolver answers `FULL` without looking at a classification, so no field access decision is made from it. Every request, on that thread or another, gets the rows the read returns, or the error. #43 has the full argument and the table of tests. ## Tests `SecuredDataRuleReloadTest`, 11 tests; 9 of them end in `StackOverflowError` without the main-code changes. ## Build `./mvnw -B verify` on this head: 112 tests, 0 failures, Checkstyle and the JaCoCo gates green. Nothing skipped. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
james.maes added 1 commit 2026-11-14 07:58:02 +00:00
fix: a rule read that asks for its own rows no longer recurses
maven / build (push) Successful in 1m22s
build / maven (push) Successful in 1m22s
db26a1dc2e
Reading the rule rows a node keeps (classifications, active retention rules,
policies) is a query on a secured-data table, and other code runs inside that
query: a virtual field value provider or a post-query customizer that another
QBit or the host put on the table. When that code runs a query with filter
criteria, core validates the operands of classified fields and asks this QBit
for the classification of the field, which needs the rows that are being
read. The version of the rows was recorded only after the read returned, so
the nested question started the read again, and that read asked again:
StackOverflowError, and the same on every later request, since nothing was
ever recorded. One classification row was enough (core runs no value provider
for an empty result).

- System reads of the QBit's own tables (SystemWrites.query, the audit key
  lookup) name the stored fields of the table. Core fills a virtual field only
  for a query that selects it, so no value provider runs inside a system read.
- RuleRows holds one kind of rule rows and is used for all three kinds, which
  had the same shape. A question asked from inside a read is answered from the
  rows held when the read began (none before the first read) and never starts
  another read. The owner's monitor makes that answer private to the reading
  thread: every other thread waits and gets the rows the read returns.
- A read that fails records nothing and clears the reading mark, so the next
  question reads again. The rows held before are not given to a request in
  place of rows that could not be read.
- RetentionRules.active: a question asked from inside a transaction's read of
  its own rule edits is answered from the rules every thread shares instead
  of reading the edits again.

Who can be given the nested answer: only code on the reading thread, inside
SystemWrites.call, where the session is the system session and the resolver
answers FULL without looking at a classification. No field access decision
for a person is made from it; the request that started the read, and every
request on another thread, gets the rows the read returns.

Tests (SecuredDataRuleReloadTest): 9 of the 11 end in StackOverflowError
without the fix. They cover a value provider and a post-query customizer on
the classification, retention rule and policy tables, the answer a nested
question gets in both directions of a change, the session it is asked in, a
second thread waiting for the read, and failed reads (an exception and an
Error) being read again.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
james.maes merged commit 1c5304992d into release/0.1 2026-11-14 08:23:09 +00:00
Sign in to join this conversation.