Skip to content

Follow-ups from #5608: the dead getBucketById null-check has siblings, monotonic stats are gauges, Studio hides zeroes, CI is permanently red #5636

Description

@lvca

Summary

Four things surfaced while doing #5608 (PR #5611, merged as bf5137ce0) that were out of its scope. Only the first is a defect with user-visible consequences; the rest are observability and hygiene items that the same work made visible. Grouped into one issue because they were all found in one pass and none is big enough to stand alone.


  • 1. The dead null-check that Follow-ups from #5596: surface the merge counters, pin the compressPage coupling, prune redundant poison calls #5608 item 4 fixed is a PATTERN, and at least one other instance costs a user a good error message.

    Follow-ups from #5596: surface the merge counters, pin the compressPage coupling, prune redundant poison calls #5608 established that LocalSchema.getBucketById(id) (single-arg, LocalSchema.java:444) raises SchemaException in exactly the cases a caller would test for null - both an out-of-range id and a component that is not a bucket - so every if (bucket == null) written after it is unreachable. The two-arg getBucketById(id, false) (:448) is the form that returns the null those branches were written against. Two more sites do the same thing:

    • BinarySerializer.readExternalValue (BinarySerializer.java:1183-1191) - the one that actually costs something. The dead branch holds a deliberately written, user-actionable SerializationException: "If the bucket was tiered to a secondary path, set 'arcadedb.externalPropertyBucketPath' to the same value used at creation time and reopen the database." That message is unreachable. A user who hits precisely the scenario its own comment describes - an EXTERNAL property bucket tiered via arcadedb.externalPropertyBucketPath, then reopened with that config unset - gets a bare Bucket with id 'N' was not found and none of the guidance someone wrote for exactly this moment. This is the hardest case in the file to diagnose from the outside, which is presumably why the message exists.

    • TruncateBucketStatement (TruncateBucketStatement.java:63-66) - cosmetic. The branch is dead too, but the enclosing catch (Exception) rewraps into the same CommandExecutionException, so the only damage is a doubled message (Bucket not found: Bucket with id '9999' was not found). Listing it for completeness, not as a reason to act.

    While checking those: BinarySerializer.finalizeExternalWrite (:1155) dereferences the bucket with no null check at all. That is not an NPE risk - the single-arg form throws first - but it means the WRITE path of the same feature raises a bare SchemaException where the READ path at least tried to explain itself. Worth making the pair consistent.

    Fix: two-arg lookup at :1183 and :63, which is the exact one-line change Follow-ups from #5596: surface the merge counters, pin the compressPage coupling, prune redundant poison calls #5608 made. Better than fixing the call sites one at a time as they are found: the one-arg form's javadoc says nothing about throwing, and its name reads like a plain accessor - that API shape is what keeps inviting the mistake. Documenting the throw on getBucketById(int), and grepping the remaining call sites once, would close the pattern rather than the instance.

  • 2. Every monotonic engine counter is exposed as a Micrometer Gauge, not a FunctionCounter.

    Raised during the fix(engine) #5608: the merge counters reach the operator, and the compression a rebase owes its page is structural #5611 review and deliberately not acted on there, because changing only the three counters that PR added would have made them inconsistent with the ten already in EngineMetricsBinder. It affects all of them: page.cache.hits, page.cache.misses, pages.read, pages.written, wal.bytes.written, mvcc.conflicts, tx.write, tx.read, tx.rollbacks, queries, commands, and now page.merges.*. Each is a never-decreasing total registered through Gauge.builder.

    Consequence in Prometheus: no _total suffix and no type hint that rate()/increase() are the correct functions over these series. A dashboard author reading arcadedb_engine_mvcc_conflicts as a gauge sees a line that only goes up and learns nothing.

    This is a deliberate, breaking decision (the metric names change), which is why it wants its own issue rather than being smuggled into a fix: converting to FunctionCounter renames the exported series, so it needs a release-note entry and probably a deprecation window.

  • 3. Studio renders a zero-valued profiler stat as absent rather than as zero.

    studio-server.js, displayMetrics, profiler-details table: the value chain is else if (entry.count != null && entry.count != 0) and otherwise continue. A counter sitting at 0 is therefore skipped entirely, so the operator cannot tell "this is zero" from "this is not reported". For a health signal that is backwards - zero is the good state and its absence is the ambiguous one. The same != 0 guard is applied to space as well.

    (The three counters added in Follow-ups from #5596: surface the merge counters, pin the compressPage coupling, prune redundant poison calls #5608 dodge this only because they went into the rate-tracked table, which has no such guard. Everything else in the details table is affected.)

  • 4. Three CI checks are red on every commit, which trains reviewers to ignore red.

    Observed across fix(engine) #5608: the merge counters reach the operator, and the compression a rebase owes its page is structural #5611 and confirmed on main's own commits:

    • Meterian client scan fails on every main commit: Unable to generate lockfile in folder /github/workspace/bindings/python -> python: python dependencies generation failed -> Failed checks: [security]. The Java scan itself passes; the Python bindings kill the run.
    • Dependency Review fails on a config error, not on a dependency: Error parsing package-url: package-url must start with "pkg:" on allow_dependencies_licenses. The workflow passes license identifiers (MIT, Apache-2.0, ...) to allow-dependencies-licenses, which expects package-URLs; the license allow-list belongs in allow-licenses. Note .github/dependency-review-config.yml also declares a licensing.allow list, so the two may be fighting each other.

    Neither has anything to do with the PRs they block. A PR author cannot distinguish "my change broke something" from "this is always red", which is the actual cost.

    For the record, a correction to something I reported mid-review: I initially attributed a third failure (Frontend Security Audit) to a vulnerable Bootstrap 4.x pin. That was wrong - studio/package.json pins ^5.3.8. The failure came from an older version of the check script, which has since been rewritten on main to parse the major version properly and no longer misfires.


Context: #5596, #5608, PR #5611 (bf5137ce0).

Activity

  1. self-assigned this
    on Jul 31, 2026
  2. added theissue type on Jul 31, 2026
  3. added this to the 26.8.1 milestone on Jul 31, 2026
  4. added 14 commits that reference this issue on Jul 31, 2026
  5. lvca commented on Aug 1, 2026

    @lvca
    MemberAuthor

    Shipped in #5640 (d322f0fe). Recording what changed relative to what was filed, because two of the four items rested on a wrong premise and a future reader of this issue would otherwise re-derive them.

    Item 2 as filed would have shipped the bug it set out to fix. "Every monotonic engine counter is a Gauge" is half right. Six of the twelve - tx.write, tx.read, tx.rollbacks, queries, commands, wal.bytes.written - are summed over the currently open databases in Profiler.toJSON(), so closing or dropping one makes them drop. A FunctionCounter over a decreasing value is read by Prometheus as a counter reset, so converting them as filed would have fabricated a rate() spike on every database close. The fix had to be monotonicity first (Profiler folds a departing database into a retained baseline), instrument type second. Two further defects surfaced underneath: unregisterDatabase mutated a plain LinkedHashSet that the synchronized toJSON() iterates, and LocalDatabase.equals/hashCode are path-derived, so an equals-based registry could not tell a closed instance from a reopened one - a stale unregister would double-fold and evict the live database.

    Item 4's diagnosis is wrong. Meterian does not die on the Python bindings. The scan completes (Execution successful!, 633 dependencies uploaded) and fails on security: 0 (minimum: 90) - real advisories, including CVE-2026-33186 (critical) in google.golang.org/grpc@v1.67.0 via e2e-go, plus ~40 npm and Java findings already tracked as code-scanning alerts. The uv lock error is logged but non-fatal (bindings/python is [tool.uv] managed = false on purpose). No single PR can move a score computed over the whole transitive graph, which is what made it permanently red; it is now continue-on-error on both the scan and the upload step - the upload matters because a fork PR gets a read-only token and would have reddened the job there instead. The decision is recorded in SECURITY.md.

    Item 1's dead-guard family was six sites, not two. The first sweep matched lookup; if (x == null) adjacency and missed every instance hiding behind an else arm - handleCreateRecord has an else bucket = null, but that arm only covers "no bucket named at all", so it was dead for exactly the input its message describes. Closed at the API instead (Schema.getBucketByIdIfExists / getBucketByNameIfExists). Chasing it further turned up two user-visible bugs the issue did not mention: SELECT FROM bucket:<id> leaked a raw SchemaException where bucket:<name> reported properly, and INSERT INTO bucket:? FROM SELECT never resolved its parameter, failing for a bucket that exists.

    Item 3 (Studio hiding zeroes) was correct as filed.

    The dependency-review half of item 4 was fixed independently and better by #5639 while this was in review, so #5640 took main's version.

  6. added a commit that references this issue on Aug 13, 2026
  7. added 2 commits that reference this issue on Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions