Skip to content

OSAC-3410: Add CaaS lifecycle metering with N+1 per-component billing - #172

Merged
omer-vishlitzky merged 19 commits into
osac-project:mainfrom
omer-vishlitzky:feat/OSAC-3410-caas-lifecycle-metering
Aug 9, 2026
Merged

OSAC-3410: Add CaaS lifecycle metering with N+1 per-component billing#172
omer-vishlitzky merged 19 commits into
osac-project:mainfrom
omer-vishlitzky:feat/OSAC-3410-caas-lifecycle-metering

Conversation

@omer-vishlitzky

@omer-vishlitzky omer-vishlitzky commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Extends the Metering Service to track CaaS (Kubernetes cluster) lifecycle events with per-component billing. Each cluster generates N+1 Kafka records per heartbeat or lifecycle transition: one for the control plane plus one per distinct worker node set.

What this PR includes

  • Declarative transition tables: Both VMaaS and CaaS state machines use TransitionTable maps instead of switch statements. The table IS the spec — missing entry = error (fail fast). Adding BMaaS = adding a new table.
  • CaaS state machine: PROGRESSING/READY billable, FAILED/DELETING/DELETE_FAILED not. ErrSkipTransition sentinel for transitions with no billing effect.
  • Transient state handling (from merged PR NO-ISSUE: fix - preserve billing context through transient states and treat DELETING as billing-ending #176): STOPPING/STARTING return ErrTransientState — projection updates only FulfillmentVersion+TransitionTime, preserving CurrentState and billing context. DELETING emits suspended.v1 (billing-ending, not transient).
  • Publish-first ordering: publishAndUpsert enforces publish-before-commit by construction. Prevents permanent event loss on partial N+1 Kafka failure. Delete path was already correct.
  • N+1 decomposition at publish time: 1 projection row per cluster, decomposed into per-component CloudEvents at publish. created.v1 and deleted.v1 are single records (audit only).
  • DecomposeClusterEvents shared helper: Extracted from 4 call sites (Watch Consumer, Heartbeat Generator, Reconciler corrections, Reconciler synthetic heartbeats). Cluster-order only, no fallback — empty components = ErrDataQuality.
  • Scaling detection: ChangedComponents emits updated.v1 only for components whose node_count differs or were added/removed.
  • Heartbeat N+1: Heartbeat Generator decomposes cluster_order into per-component heartbeats with flat billing dimensions.
  • Reconciliation: ClusterLister interface, paginated ListClusters, isBillableForType dispatch, N+1 correction events, N+1 synthetic heartbeats.
  • Deterministic CloudEvent IDs: {eventID}/{nodeSet} for adapter-level dedup on replay.

Tickets covered

Ticket Summary Coverage
OSAC-3421 CaaS state machine + Watch Consumer Fully met
OSAC-3433 CaaS heartbeat + reconciliation Fully met
OSAC-3438 CaaS E2E tests Separate PR (osac-test-infra)

Design reference

Key design decisions

Decision Choice Why
State machines Declarative TransitionTable maps Table IS the spec, missing entry = error, no switch defaults to get wrong
Publish ordering Publish-first via publishAndUpsert Prevents permanent event loss on partial N+1 Kafka failure
N+1 decomposition DecomposeClusterEvents shared helper Single implementation, cluster-only, no fallback, empty = error
Projection model 1 row per cluster Simpler reconciliation; N+1 at publish time
Billing dims storage components array in JSONB Enables DimensionsEqual for scaling detection
created/deleted decomposition Single record Audit only, no billing effect (per spec)
CloudEvent IDs {eventID}/{nodeSet} Deterministic for adapter dedup on replay
Node sets source spec.node_sets Billing follows desired state, not status
Transient states (VMaaS) STOPPING/STARTING preserve CurrentState previous_state=RUNNING on suspended.v1, matching design exactly
DELETING (VMaaS) Emits suspended.v1 Billing-ending, not transient — closes interval for direct deletes
DELETING (CaaS) suspended.v1 only from billable states Non-billable→DELETING has no interval to close
ErrSkipTransition Sentinel error Forgotten check = reconnect, not silent data loss
Initial non-billable (CaaS) ""→FAILED/DELETING/etc = Skip Prevents crash on bootstrap/reconnect with existing clusters

Review fixes included

Fixes from 5-agent critical review + amito's inline comments:

  1. Publish-first orderingpublishAndUpsert enforces publish before commit, prevents event loss on partial N+1 Kafka failure (affected both VMaaS and CaaS)
  2. Missing initial CaaS transitions""→FAILED/DELETING/DELETE_FAILED/UNSPECIFIED added as Skip (prevents crash on first-observed non-billable cluster)
  3. ErrSkipNonBillingTransition renamed to ErrSkipTransition — old name misleading for billable→billable transitions
  4. DecomposeClusterEvents shared helper — extracted from 4 call sites per amito's review; cluster-only, no fallback, empty components = ErrDataQuality
  5. Proto enumeration completeness test — iterates every (from, to) state pair, catches removed table entries
  6. DimensionsEqual component ordering test — documents that order matters (sorted keys for determinism)
  7. Strengthened multi-removal test — verifies exact NodeSet→HostType preservation

Test plan

  • go build ./... passes
  • SKIP_DB_TESTS=1 ginkgo run -r --timeout=1m internal/ — 240 specs pass (7 suites, ~4s)
  • Pre-commit (golangci-lint) passes
  • VMaaS transition table: 10 entries + wildcard expansions, all tested + unknown state error
  • CaaS transition table: 42 entries, 42 test entries (1:1) + unknown state error
  • Proto enumeration completeness test: every (from, to) state pair covered
  • DecomposeClusterEvents: happy path, empty components error, buildFn error propagation
  • publishAndUpsert: stale version test updated for publish-first semantics
  • Deploy to dev cluster with CaaS cluster, verify N+1 events on lifecycle/heartbeat topics

Assisted-by: Claude Code noreply@anthropic.com

Summary by CodeRabbit

  • New Features
    • Added metering support for CaaS clusters, including lifecycle events, billability, metadata, and state transitions.
    • Added component-level billing for control-plane and worker node-set changes, including scaling, additions, and removals.
    • Added cluster reconciliation, correction events, and synthetic heartbeats for all billable components.
  • Bug Fixes
    • Improved event ordering, projection updates, stale-version handling, and checkpointing when publishing multiple events.
    • Added clearer validation for missing timestamps and unsupported transitions.
  • Tests
    • Expanded coverage for cluster lifecycle, scaling, reconciliation, corrections, heartbeats, and transition handling.

@openshift-ci-robot

openshift-ci-robot commented Aug 6, 2026

Copy link
Copy Markdown

@omer-vishlitzky: This pull request references OSAC-3410 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the epic to target the "5.0.0" version, but no target version was set.

Details

In response to this:

Summary

Extends the Metering Service to track CaaS (Kubernetes cluster) lifecycle events with per-component billing. Each cluster generates N+1 Kafka records per heartbeat or lifecycle transition: one for the control plane plus one per distinct worker node set.

What this PR includes

  • CaaS state machine: PROGRESSING/READY billable, FAILED/DELETING/DELETE_FAILED not. ErrSkipNonBillingTransition sentinel for transitions with no billing effect.
  • N+1 decomposition at publish time: 1 projection row per cluster, decomposed into per-component CloudEvents at publish. created.v1 and deleted.v1 are single records (audit only).
  • Scaling detection: ChangedComponents emits updated.v1 only for components whose node_count differs or were added/removed.
  • Heartbeat N+1: Heartbeat Generator decomposes cluster_order into per-component heartbeats with flat billing dimensions.
  • Reconciliation: ClusterLister interface, paginated ListClusters, isBillableForType dispatch, N+1 correction events, N+1 synthetic heartbeats.
  • Deterministic CloudEvent IDs: {eventID}/{component}:{host_type} for adapter-level dedup on replay.
  • 58 new specs: Full state transition matrix, JSONB round-trip, decomposition, scaling detection.

Tickets covered

Ticket Summary Coverage
OSAC-3421 CaaS state machine + Watch Consumer Fully met
OSAC-3433 CaaS heartbeat + reconciliation Fully met
OSAC-3438 CaaS E2E tests Separate PR (osac-test-infra)

Design reference

Key design decisions

Decision Choice Why
Projection model 1 row per cluster Simpler reconciliation; N+1 at publish time
Billing dims storage components array in JSONB Enables DimensionsEqual for scaling detection
created/deleted decomposition Single record Audit only, no billing effect (per spec)
CloudEvent IDs {eventID}/{component}:{host_type} Deterministic for adapter dedup on replay
Node sets source spec.node_sets Billing follows desired state, not status
ErrSkipNonBillingTransition Sentinel error Forgotten check = reconnect, not silent data loss

Test plan

  • go build ./... passes
  • SKIP_DB_TESTS=1 ginkgo run -r internal/ — 184 specs pass (7 suites)
  • go vet ./... passes
  • Deploy to dev cluster with CaaS cluster, verify N+1 events on lifecycle/heartbeat topics

Assisted-by: Claude Code noreply@anthropic.com

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The metering service now supports CaaS cluster events and component-level lifecycle, scaling, heartbeat, and correction events. Reconciliation loads clusters through a new client, while watch and heartbeat paths publish decomposed component events.

Changes

Cluster metering

Layer / File(s) Summary
Shared event transitions and resource dispatch
osac-metering/metering-service/internal/events/transitions.go, osac-metering/metering-service/internal/events/compute_instance.go, osac-metering/metering-service/internal/events/mapper.go
Shared transition resolution, timestamp validation, resource decomposition, and explicit compute-instance state transitions are added.
Cluster event mapping and component dimensions
osac-metering/metering-service/internal/events/cluster.go, osac-metering/metering-service/internal/events/cluster_test.go
Cluster payloads map to lifecycle events with billability, timestamps, billing dimensions, deterministic component IDs, decomposition, and change detection.
Watch filtering and component lifecycle events
osac-metering/metering-service/internal/watch/*
The watch consumer accepts cluster events and publishes component lifecycle and scaling events while updating projections.
Multi-event heartbeat generation
osac-metering/metering-service/internal/heartbeat/*
Cluster heartbeats produce one event per component and checkpoint resources only after all publications succeed.
Cluster reconciliation and corrections
osac-metering/metering-service/internal/reconciliation/*, osac-metering/metering-service/cmd/metering-service/main.go, osac-metering/metering-service/go.mod
Reconciliation loads paginated clusters, selects resource-specific billability, publishes component corrections, and receives a clusters client from service startup.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WatchStream
  participant WatchConsumer
  participant ClusterMapper
  participant Projection
  participant EventPublisher
  WatchStream->>WatchConsumer: cluster event
  WatchConsumer->>ClusterMapper: map cluster and detect transition
  ClusterMapper-->>WatchConsumer: component records and event metadata
  WatchConsumer->>EventPublisher: publish component lifecycle or scaling events
  WatchConsumer->>Projection: update cluster projection
Loading

Possibly related PRs

Suggested reviewers: tzvatot, avishayt


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
No-Sensitive-Data-In-Logs ❌ Error CaaS paths log raw resource_id, and published-event logs emit raw tenant_id and event_id; production zap logging has no redaction. Do not log tenant, resource, or event identifiers directly. Use approved redacted or keyed-hash correlation values, and review wrapped errors for the same identifiers.
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (9 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: CaaS lifecycle metering with per-component billing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
No-Hardcoded-Secrets ✅ Passed The complete PR diff adds no credential-named string assignments, embedded-credential URLs, private-key material, vendor token patterns, or base64/hex literals over 32 characters.
No-Weak-Crypto ✅ Passed Changed lines contain no MD5, SHA1, DES, 3DES, RC4, Blowfish, ECB, custom crypto, or secret comparisons; IDs use UUIDs and string formatting.
No-Injection-Vectors ✅ Passed No SQL injection, shell injection, eval/exec, YAML/pickle, or other injection vectors found in the PR. All fmt.Sprintf operations use safe sources (internal values, proto fields, constants).
Container-Privileges ✅ Passed The PR changes no container or Kubernetes manifests. Metering deployments enforce runAsNonRoot, drop all capabilities, and set allowPrivilegeEscalation false; no listed host flags or SYS_ADMIN appear.
Ai-Attribution ✅ Passed AI use is disclosed as “Assisted-by: Claude Code”; relevant commits use the same trailer, and no AI Co-Authored-By trailer was found.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (6)
osac-metering/metering-service/internal/watch/consumer_test.go (1)

375-397: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The filter assertion is updated, but no test exercises the cluster path it enables.

This suite now admits cluster events through the filter, yet every scenario still uses a compute instance. The new behaviour in osac-metering/metering-service/internal/watch/consumer.go is untested here:

  • publishLifecycleEvents fan-out: one cluster started.v1 should produce N+1 published events with flattened billing_dimensions.
  • handleScalingEvent: a PROGRESSINGREADY transition with a changed node set size should publish updated.v1 for the changed component only.

Add both cases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@osac-metering/metering-service/internal/watch/consumer_test.go` around lines
375 - 397, Add consumer tests covering cluster events enabled by the updated
watch filter: verify one cluster started.v1 event is fanned out by
publishLifecycleEvents into N+1 published events with flattened
billing_dimensions, and verify handleScalingEvent publishes updated.v1 only for
the component whose node-set size changes across a PROGRESSING-to-READY
transition. Use the existing mock publisher, watch stream, and event fixtures
established in this test suite.
osac-metering/metering-service/internal/events/cluster_test.go (1)

462-553: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add cases for duplicate host_type and for non-FAILED recovery.

The suite covers single-node-set changes well. Two gaps remain, both matching issues raised in osac-metering/metering-service/internal/events/cluster.go:

  • Two node sets with the same host_type and different sizes. ChangedComponents keys on component + ":" + host_type, so the records collapse.
  • DELETING or DELETE_FAILEDREADY in the transition matrix at Lines 45-95. That path currently returns osac.resource.updated.v1.

Add both cases so the fixes stay locked in.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@osac-metering/metering-service/internal/events/cluster_test.go` around lines
462 - 553, Add tests in the ChangedComponents suite for two worker node sets
sharing the same host_type but having different node counts, asserting both
records remain distinct rather than collapsing. Also add a transition test
covering DELETING and DELETE_FAILED recovering to READY, asserting the result is
osac.resource.updated.v1, using the existing transition-matrix test structure
and symbols.
osac-metering/metering-service/internal/reconciliation/reconciler.go (1)

264-266: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The cluster skip is silent.

The guard is correct: without clusterClient, fulfillmentState never holds clusters, so every cluster projection would look deleted. But if clusterClient is nil by misconfiguration, cluster deletion reconciliation stops with no signal at all.

Log once per run when the reconciler runs without a cluster lister.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@osac-metering/metering-service/internal/reconciliation/reconciler.go` around
lines 264 - 266, Add a once-per-reconciliation-run warning when the reconciler
lacks a cluster lister, before or alongside the cluster skip guard in the
reconciliation method. Ensure the warning is emitted once per run—not once per
cluster projection—while preserving the existing continue behavior for
cluster_order resources when r.clusterClient is nil.
osac-metering/metering-service/internal/heartbeat/generator.go (1)

156-184: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

buildComponentHeartbeat duplicates buildHeartbeatEvent.

Only two things differ: the event ID and BillingDimensions. Everything else — source, type, time, extensions, duration, schema version — is copied. Take the ID and the dimensions as parameters on the existing builder instead.

♻️ Proposed shape
-func (g *Generator) buildHeartbeatEvent(state *projection.ResourceState, now time.Time) (cloudevents.Event, error) {
+func (g *Generator) buildHeartbeatEvent(state *projection.ResourceState, eventID string, dims map[string]any, now time.Time) (cloudevents.Event, error) {
 	ce := cloudevents.NewEvent()
-	ce.SetID(uuid.NewString())
+	ce.SetID(eventID)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@osac-metering/metering-service/internal/heartbeat/generator.go` around lines
156 - 184, Update the existing buildHeartbeatEvent builder to accept the event
ID and BillingDimensions as parameters, then reuse it from
buildComponentHeartbeat instead of duplicating event construction. Preserve the
shared source, type, time, extensions, duration, schema version, data-setting,
and error-handling behavior while passing the component-specific values.
osac-metering/metering-service/internal/reconciliation/reconciler_test.go (1)

162-162: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Every call site passes nil, so no cluster reconciliation path is tested.

The mechanical update is correct, but it leaves the entire new surface uncovered: loadClusters pagination, isBillableForType for cluster_order, component-decomposed corrections, and the clusterClient == nil skip in reconcileMissedDeletions.

Add a mockClusterClient and at least these cases:

  • a cluster present in fulfillment but absent from the projection, asserting N+1 correction events;
  • a cluster projection with clusterClient set to nil, asserting no missed_deletion is emitted.

A small newReconciler(client, store, pub) helper would also remove the bare nil from 15 call sites.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@osac-metering/metering-service/internal/reconciliation/reconciler_test.go` at
line 162, Expand reconciler tests by adding a mockClusterClient and a
newReconciler(client, store, pub) helper, then replace the existing bare nil
cluster-client arguments at all call sites. Add coverage for loadClusters
pagination, cluster_order billability, component-decomposed corrections, a
fulfillment cluster missing from the projection producing N+1 correction events,
and a cluster projection with nil clusterClient producing no missed_deletion
event.
osac-metering/metering-service/internal/watch/consumer.go (1)

299-312: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The scaling payload duplicates meteringData as an untyped map.

events.MapWatchEvent builds the same payload from the meteringData struct with JSON tags. This function rebuilds the identical field set by hand. Any future change to meteringData (a renamed field, a new schema_version) will silently skip this path, and the two producers will emit different shapes on the same topic.

Export a shared builder in the events package and call it from both places.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@osac-metering/metering-service/internal/watch/consumer.go` around lines 299 -
312, The scaling payload construction near the data map must stop duplicating
meteringData fields manually. Add an exported shared builder in the events
package that produces the tagged meteringData payload, update
events.MapWatchEvent and the scaling path to call it, and preserve the existing
field values and serialized shape for both producers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/build-metering-test-adapter-image.yaml:
- Around line 51-54: Update the version-tagging step around RELEASE_VERSION,
RELEASE_MAJOR_MINOR, and RELEASE_MAJOR so rolling tags are written only when
version has no prerelease suffix. Continue exporting RELEASE_VERSION for all
valid versions, but guard the major/minor tag assignments and corresponding
image publishing so values like v1.2.3-rc.1 cannot produce or overwrite v1.2 or
v1 tags.
- Around line 26-40: Update the workflow so the build job uses only read
permissions, while `packages: write` is granted exclusively to a separate
publish-only job. In the `actions/checkout` step, set `persist-credentials` to
false, and verify `.dockerignore` excludes `.git` so repository metadata is not
included in the Docker build context.

In `@osac-metering/metering-service/internal/events/cluster.go`:
- Around line 110-123: Update the transition switch to classify every
non-billable-to-billable transition as osac.resource.resumed.v1 by using the
computed previousBillable value, while preserving the existing started.v1
handling for an empty previous state and other transition outcomes.
- Around line 185-201: Extend ComponentRecord with a NodeSet field, populate it
from each node-set map key in DecomposeClusterComponents, and include node_set
in the identity key used by ComponentEventID and ChangedComponents. Preserve
deterministic ordering and ensure the key format is component + ":" + node_set +
":" + host_type so distinct node sets cannot collide.

In `@osac-metering/metering-service/internal/reconciliation/correction.go`:
- Around line 76-91: Make component event IDs deterministic per logical emission
so replay deduplication works: in
osac-metering/metering-service/internal/reconciliation/correction.go lines
76-91, derive one base ID from resourceID, reason, and now before the component
loop and reuse it with ComponentEventID; in
osac-metering/metering-service/internal/reconciliation/reconciler.go lines
467-487, update buildSyntheticHeartbeats to derive the base from ps.ResourceID
and now instead of uuid.NewString(); in
osac-metering/metering-service/internal/heartbeat/generator.go lines 105-117,
update buildComponentHeartbeat to derive it from state.ResourceID and now
instead of uuid.NewString().

In `@osac-metering/metering-service/internal/watch/consumer.go`:
- Around line 199-209: Update Consumer.publishLifecycleEvents so cluster_order
delete events do not take the base-event early return; retain that behavior for
created events and non-cluster resources. Route deleted events through
DecomposeClusterComponents and the existing per-component publishing path,
preserving one flattened event for each component while leaving other lifecycle
transitions unchanged.

---

Nitpick comments:
In `@osac-metering/metering-service/internal/events/cluster_test.go`:
- Around line 462-553: Add tests in the ChangedComponents suite for two worker
node sets sharing the same host_type but having different node counts, asserting
both records remain distinct rather than collapsing. Also add a transition test
covering DELETING and DELETE_FAILED recovering to READY, asserting the result is
osac.resource.updated.v1, using the existing transition-matrix test structure
and symbols.

In `@osac-metering/metering-service/internal/heartbeat/generator.go`:
- Around line 156-184: Update the existing buildHeartbeatEvent builder to accept
the event ID and BillingDimensions as parameters, then reuse it from
buildComponentHeartbeat instead of duplicating event construction. Preserve the
shared source, type, time, extensions, duration, schema version, data-setting,
and error-handling behavior while passing the component-specific values.

In `@osac-metering/metering-service/internal/reconciliation/reconciler_test.go`:
- Line 162: Expand reconciler tests by adding a mockClusterClient and a
newReconciler(client, store, pub) helper, then replace the existing bare nil
cluster-client arguments at all call sites. Add coverage for loadClusters
pagination, cluster_order billability, component-decomposed corrections, a
fulfillment cluster missing from the projection producing N+1 correction events,
and a cluster projection with nil clusterClient producing no missed_deletion
event.

In `@osac-metering/metering-service/internal/reconciliation/reconciler.go`:
- Around line 264-266: Add a once-per-reconciliation-run warning when the
reconciler lacks a cluster lister, before or alongside the cluster skip guard in
the reconciliation method. Ensure the warning is emitted once per run—not once
per cluster projection—while preserving the existing continue behavior for
cluster_order resources when r.clusterClient is nil.

In `@osac-metering/metering-service/internal/watch/consumer_test.go`:
- Around line 375-397: Add consumer tests covering cluster events enabled by the
updated watch filter: verify one cluster started.v1 event is fanned out by
publishLifecycleEvents into N+1 published events with flattened
billing_dimensions, and verify handleScalingEvent publishes updated.v1 only for
the component whose node-set size changes across a PROGRESSING-to-READY
transition. Use the existing mock publisher, watch stream, and event fixtures
established in this test suite.

In `@osac-metering/metering-service/internal/watch/consumer.go`:
- Around line 299-312: The scaling payload construction near the data map must
stop duplicating meteringData fields manually. Add an exported shared builder in
the events package that produces the tagged meteringData payload, update
events.MapWatchEvent and the scaling path to call it, and preserve the existing
field values and serialized shape for both producers.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: osac-project/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 87c672d3-0b23-4c1d-bb89-d29bff00d787

📥 Commits

Reviewing files that changed from the base of the PR and between 86a0eba and e787560.

📒 Files selected for processing (13)
  • .github/workflows/build-metering-test-adapter-image.yaml
  • .github/workflows/e2e-vmaas-full-install.yml
  • osac-metering/metering-service/cmd/metering-service/main.go
  • osac-metering/metering-service/internal/events/cluster.go
  • osac-metering/metering-service/internal/events/cluster_test.go
  • osac-metering/metering-service/internal/events/mapper.go
  • osac-metering/metering-service/internal/events/mapper_test.go
  • osac-metering/metering-service/internal/heartbeat/generator.go
  • osac-metering/metering-service/internal/reconciliation/correction.go
  • osac-metering/metering-service/internal/reconciliation/reconciler.go
  • osac-metering/metering-service/internal/reconciliation/reconciler_test.go
  • osac-metering/metering-service/internal/watch/consumer.go
  • osac-metering/metering-service/internal/watch/consumer_test.go

Comment thread .github/workflows/build-metering-test-adapter-image.yaml
Comment thread .github/workflows/build-metering-test-adapter-image.yaml
Comment thread osac-metering/metering-service/internal/events/cluster.go Outdated
Comment thread osac-metering/metering-service/internal/events/cluster.go
Comment thread osac-metering/metering-service/internal/reconciliation/correction.go Outdated
Comment thread osac-metering/metering-service/internal/watch/consumer.go
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
osac-metering/metering-service/internal/heartbeat/generator_test.go (1)

281-313: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the N+1 heartbeat tests deterministic.

gen.Run can execute multiple ticks during the 300 ms timeout. The lower-bound counts and accumulated component map can pass if components publish across separate ticks. The checkpoint assertion can also pass after an incomplete tick.

Drive exactly one tick with a controlled ticker or a one-shot store response. Then assert the exact event count and checkpoint state for that tick.

Also applies to: 315-340, 342-358

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@osac-metering/metering-service/internal/heartbeat/generator_test.go` around
lines 281 - 313, Make the N+1 heartbeat tests deterministic by replacing the
real-time 300 ms gen.Run execution with a controlled single-tick mechanism,
using the generator’s ticker or a one-shot store response. Update the tests
around the N+1 heartbeat case and the related cases at lines 315-340 and 342-358
to assert the exact heartbeat count, component set, and checkpoint state
produced by that one tick.
osac-metering/metering-service/internal/events/cluster.go (1)

298-304: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Change detection ignores host_type changes.

The comparison only tests NodeCount. If a node set keeps its size but changes host_type, no component event is emitted. Billing then uses the old host type until the hourly reconciler reports billing_dimensions_drift. The comment on Lines 97-104 documents that fallback, so the gap is bounded, not permanent. A direct comparison closes it immediately.

♻️ Proposed change
 		old, exists := oldByKey[r.NodeSet]
-		if !exists || old.NodeCount != r.NodeCount {
+		if !exists || old.NodeCount != r.NodeCount || old.HostType != r.HostType {
 			changed = append(changed, r)
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@osac-metering/metering-service/internal/events/cluster.go` around lines 298 -
304, Update the change detection loop over newRecords to also compare each
record’s host_type against the matching old record, alongside NodeCount. Append
the record to changed whenever either value differs, while preserving the
existing handling for new node sets.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@osac-metering/metering-service/internal/events/cluster.go`:
- Around line 306-316: Update the removal branch in the oldRecords comparison to
copy r.NodeSet into each zero-count ComponentRecord, preserving the node-set
identity used by ComponentEventID and FlatBillingDimensions for distinct removal
events and billing closure.

In `@osac-metering/metering-service/internal/reconciliation/reconciler.go`:
- Around line 472-492: Update buildSyntheticHeartbeats to derive one
deterministic base event ID from the resource and heartbeat window, truncating
now to the heartbeat interval, then reuse that base ID for every component and
suffix it via ComponentEventID. Ensure retries within the same stale-heartbeat
window regenerate identical component IDs while preserving the single-heartbeat
path for non-cluster resources.

---

Nitpick comments:
In `@osac-metering/metering-service/internal/events/cluster.go`:
- Around line 298-304: Update the change detection loop over newRecords to also
compare each record’s host_type against the matching old record, alongside
NodeCount. Append the record to changed whenever either value differs, while
preserving the existing handling for new node sets.

In `@osac-metering/metering-service/internal/heartbeat/generator_test.go`:
- Around line 281-313: Make the N+1 heartbeat tests deterministic by replacing
the real-time 300 ms gen.Run execution with a controlled single-tick mechanism,
using the generator’s ticker or a one-shot store response. Update the tests
around the N+1 heartbeat case and the related cases at lines 315-340 and 342-358
to assert the exact heartbeat count, component set, and checkpoint state
produced by that one tick.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: osac-project/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: bf809d1f-4c29-46e0-9073-54e8d576e228

📥 Commits

Reviewing files that changed from the base of the PR and between 86a0eba and b415302.

⛔ Files ignored due to path filters (2)
  • go.work.sum is excluded by !**/*.sum
  • osac-metering/metering-service/go.sum is excluded by !**/*.sum
📒 Files selected for processing (14)
  • osac-metering/metering-service/cmd/metering-service/main.go
  • osac-metering/metering-service/go.mod
  • osac-metering/metering-service/internal/events/cluster.go
  • osac-metering/metering-service/internal/events/cluster_test.go
  • osac-metering/metering-service/internal/events/compute_instance.go
  • osac-metering/metering-service/internal/events/mapper.go
  • osac-metering/metering-service/internal/events/mapper_test.go
  • osac-metering/metering-service/internal/heartbeat/generator.go
  • osac-metering/metering-service/internal/heartbeat/generator_test.go
  • osac-metering/metering-service/internal/reconciliation/correction.go
  • osac-metering/metering-service/internal/reconciliation/reconciler.go
  • osac-metering/metering-service/internal/reconciliation/reconciler_test.go
  • osac-metering/metering-service/internal/watch/consumer.go
  • osac-metering/metering-service/internal/watch/consumer_test.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • osac-metering/metering-service/internal/events/mapper_test.go
  • osac-metering/metering-service/cmd/metering-service/main.go
  • osac-metering/metering-service/internal/reconciliation/correction.go
  • osac-metering/metering-service/internal/heartbeat/generator.go
  • osac-metering/metering-service/internal/events/mapper.go
  • osac-metering/metering-service/internal/watch/consumer.go
  • osac-metering/metering-service/internal/events/cluster_test.go

Comment thread osac-metering/metering-service/internal/events/cluster.go
Implement ResourceMapper for ClusterOrder with CaaS-specific billing:
- State machine: PROGRESSING/READY billable, FAILED/DELETING/DELETE_FAILED not
- ErrSkipNonBillingTransition sentinel for billable<->billable and
  non-billable<->non-billable transitions
- ClusterBillingDimensions with full components array (control plane +
  sorted worker node sets from spec)
- DecomposeClusterComponents for N+1 fan-out at publish time
- ChangedComponents for scaling detection (including removal with NodeCount=0)
- ComponentEventID for deterministic adapter-level dedup
- 58 new specs covering full state transition matrix, JSONB round-trip,
  decomposition, and scaling detection

Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: omer-vishlitzky <omer.vishlitzky@gmail.com>
…aling

- Extend Watch filter to include cluster events
- ErrSkipNonBillingTransition handling: update projection on non-billing
  transitions, detect dimension changes for scaling events
- publishLifecycleEvents: single event for VMaaS/created/deleted, N+1
  decomposition for CaaS lifecycle events
- handleScalingEvent: emit updated.v1 only for changed components,
  update projection even when only non-component dims change
- Deterministic CloudEvent IDs via ComponentEventID for adapter dedup

Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: omer-vishlitzky <omer.vishlitzky@gmail.com>
Heartbeat Generator:
- buildHeartbeatEvents returns 1 event for VMaaS, N+1 for cluster_order
- Per-component heartbeats with flat billing dimensions

Reconciler:
- Add ClusterLister interface and loadClusters pagination
- isBillableForType dispatch for per-resource-type billability
- Generalize hardcoded "compute_instance" strings to fs.resourceType
- N+1 decomposition for correction events via buildCorrectionEvents
- N+1 synthetic heartbeats via buildSyntheticHeartbeats
- Guard against nil clusterClient in missed deletion detection
- Wire ClustersClient in main.go

Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: omer-vishlitzky <omer.vishlitzky@gmail.com>
Consumer tests (8 new specs):
- Cluster CREATED publishes exactly 1 event (not N+1)
- Cluster started.v1 publishes N+1 events (3 for 1 CP + 2 workers)
- Each decomposed event has distinct per-component billing_dimensions
- Each decomposed event has deterministic component-scoped ID
- PROGRESSING→READY skips publish but updates projection
- READY→FAILED publishes N+1 suspended.v1
- Scaling publishes updated.v1 only for changed component
- Cluster DELETED publishes exactly 1 event

Heartbeat tests (4 new specs):
- cluster_order produces N+1 heartbeats with flat billing_dimensions
- VMaaS and CaaS in same tick produce correct event counts
- Cluster ID checkpointed after all component heartbeats
- Partial N+1 publish failure does not checkpoint

Reconciler tests (5 new specs):
- Cluster missed_creation emits N+1 correction events with flat dims
- Cluster state_drift emits N+1 correction events
- Cluster missed_deletion emits N+1 correction events
- Nil clusterClient skips cluster missed_deletion
- ListClusters pagination with 600 clusters

Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: omer-vishlitzky <omer.vishlitzky@gmail.com>
…ient log

- Sync go.work.sum (pre-commit fix)
- Consolidate buildComponentHeartbeat into buildHeartbeatEvent with
  eventID and dims params (CodeRabbit feedback)
- Log once per reconciliation run when clusterClient is nil

Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: omer-vishlitzky <omer.vishlitzky@gmail.com>
…nistic correction IDs

State machines (CaaS + VMaaS):
- CaaS: replace default updated.v1 with explicit non-billable→billable
  as resumed.v1. Default case now errors on unexpected transitions.
- VMaaS: enumerate all known states explicitly (STARTING, STOPPING,
  DELETING, DELETE_FAILED, UNSPECIFIED). Default case errors.

NodeSet-keyed components:
- Add NodeSet field to ComponentRecord, populated from spec.node_sets
  map key. Prevents host_type collision when two pools share a type.
- ComponentEventID uses NodeSet as unique key: {eventID}/{nodeSet}
- ChangedComponents keys on NodeSet instead of component:host_type
- FlatBillingDimensions includes node_set field

Deterministic correction event IDs:
- Derive base ID from resourceID + reason + states instead of
  uuid.NewString(). Same drift across reconciliation cycles produces
  same event IDs, enabling adapter-level dedup.

Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: omer-vishlitzky <omer.vishlitzky@gmail.com>
Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: omer-vishlitzky <omer.vishlitzky@gmail.com>
- correctionDescription returns error for unknown CorrectionReason
- isBillableForType returns error for unknown resource type
- No silent fallbacks remain in any switch statement

Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: omer-vishlitzky <omer.vishlitzky@gmail.com>
…, proto compat

- ChangedComponents: preserve NodeSet on removed component records so
  ComponentEventID produces unique IDs and FlatBillingDimensions carries
  the correct node_set for billing closure
- buildSyntheticHeartbeats: derive deterministic base ID from
  resourceID + timestamp instead of uuid.NewString, enabling adapter
  dedup on retry
- Adapt to typed proto references (ClusterTemplateReference,
  ClusterCatalogItemReference, HostTypeReference, VersionName)
- Fix goimports formatting

Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: omer-vishlitzky <omer.vishlitzky@gmail.com>
…tables

State transitions are now defined as maps — the table IS the spec.
Missing entry = error (fail fast). No default branches to get wrong.
Adding BMaaS or any new resource type = adding a new table.

VMaaS (compute_instance): transient states (STOPPING/STARTING),
DELETING as billing-ending, explicit wildcard fallbacks.

CaaS (cluster_order): billable-to-billable skip, non-billable skip,
same-state transitions for scaling, explicit per-state entries.

Shared lookup in transitions.go: exact (from, to) match first,
then (*,to) wildcard, then error.

Signed-off-by: omer-vishlitzky <ovishliz@redhat.com>
Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: omer-vishlitzky <omer.vishlitzky@gmail.com>
Fixes review findings from 5-agent critical review:

1. publishAndUpsert: all create/update paths now publish events BEFORE
   committing projection state. If Kafka fails, projection is not
   committed, replay retries full publish. Prevents permanent event
   loss on partial N+1 publish failure. Delete path was already
   correct (publish-first from PR 135 review).

2. Missing initial CaaS transitions: added ""->FAILED/DELETING/
   DELETE_FAILED/UNSPECIFIED as Skip. Prevents consumer crash when
   first-observed cluster is in non-billable state (bootstrap,
   reconnect after failure, existing deployment).

3. ErrSkipNonBillingTransition renamed to ErrSkipTransition — the old
   name was misleading for billable-to-billable transitions
   (PROGRESSING->READY).

4. Strengthened multi-removal test: verifies exact NodeSet->HostType
   preservation, not just non-empty NodeSet.

5. DimensionsEqual component ordering test: documents that array order
   matters (ClusterBillingDimensions sorts keys for determinism).

6. Proto enumeration completeness test: iterates every (from, to) state
   pair from all ClusterState proto values plus empty initial. If someone
   removes a table entry, this test fails — prevents wildcard masking.

7. Stale version test updated: publish-first means events reach Kafka
   even when projection upsert is skipped (adapter dedup handles it).

Signed-off-by: omer-vishlitzky <ovishliz@redhat.com>
Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: omer-vishlitzky <omer.vishlitzky@gmail.com>
Addresses amito's review comment: the N+1 decompose-build-publish
pattern repeated across publishLifecycleEvents, buildCorrectionEvents,
buildSyntheticHeartbeats, and buildHeartbeatEvents.

DecomposeClusterEvents handles only cluster_order — no fallback.
Empty components returns ErrDataQuality (fail fast, not silent
single-event degradation). Callers branch on resource type and
call DecomposeClusterEvents only for cluster_order.

Signed-off-by: omer-vishlitzky <ovishliz@redhat.com>
Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: omer-vishlitzky <omer.vishlitzky@gmail.com>
…rals

Zero switch statements in production code. Zero wildcards. Zero
hardcoded string literals. All state machines, event types, resource
types, timestamps, corrections, and decomposition use map-based
dispatch with constants. Missing entry = error, no fallback.

Changes:
- Constants for all event types (EventCreated, EventSuspended, ...),
  resource types (ResourceTypeComputeInstance, ResourceTypeClusterOrder),
  VMaaS states (CIState*), CaaS states (CLState*)
- ResolveCloudEventType: map-based, replaces identical switch in both
  mappers. CREATED/DELETED fixed, UPDATED delegates to transition table
- ResolveTransitionTime: map-based, replaces identical switch in both
  mappers. Unknown event type = error, nil timestamp = ErrDataQuality
- BuildResourceEvents: map-based decomposition dispatch, replaces
  5 separate "if cluster_order" branches. Unknown resource type = error.
  Adding BMaaS = one line in the map
- VMaaS transition table: 72 explicit entries (9 from × 8 to), no
  wildcards. Fixes latent bugs: ""→STOPPED was suspended.v1 (should be
  skip, no interval to close), RUNNING→UNSPECIFIED was updated.v1
  (should be suspended.v1, billable→non-billable)
- correctionDescription: map replaces switch, unknown reason = error
- isBillableForType: map replaces switch, unknown type = false

318 specs pass (was 240). New test files: transitions_test.go (18 specs),
correction_internal_test.go (5 specs). VMaaS exhaustive DescribeTable
(72 entries) + proto enumeration completeness test.

Signed-off-by: omer-vishlitzky <ovishliz@redhat.com>
Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: omer-vishlitzky <omer.vishlitzky@gmail.com>
…nsumer

The Watch Consumer crashed and reconnected on every OBJECT_SIGNALED
event (7 per E2E run) and every metadata-only OBJECT_UPDATED without
state_transition_time (3 per run). Each crash lost the bad event and
all subsequent events on that stream, since the Watch protocol has
no resume token.

Two targeted fixes — everything else still fails fast:

1. SIGNALED: ResolveCloudEventType and ResolveTransitionTime return
   ErrUnsupportedEvent. The consumer skips the event, increments
   osac_metering_events_skipped_total{reason=unsupported_event_type},
   and continues processing the same stream.

2. Metadata-only updates: when TransitionTime returns ErrDataQuality
   but the resource state hasn't changed (same as projection), the
   consumer skips silently — no state transition occurred, nothing
   to meter.

Real data quality issues (state changed but no timestamp, missing
resource_id, missing tenant_id) still crash the stream so they
surface as fulfillment-service bugs.

Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: omer-vishlitzky <omer.vishlitzky@gmail.com>
Finding 1 (CRITICAL): VMaaS dimension change was silently dropped.
handleScalingEvent called ChangedComponents (CaaS-specific), returned
empty for VMaaS, updated projection without publishing. Fixed with
BuildDimensionChangeEvents map-based dispatch: VMaaS emits single
updated.v1, CaaS emits per-changed-component events. Also fixed
ChangedComponents to compare HostType (not just NodeCount).

Finding 2 (HIGH): billing dimension field renamed from version_name
to release_image, matching the design doc and CaaS events spec gist
adapter contract.

Finding 3 (MEDIUM): cluster_order created.v1/deleted.v1 now have flat
billing_dimensions (cluster_template, release_image only), no nested
components array. Matches design spec for audit events.

Finding 4 (MEDIUM): non-billable→billable transitions now consistently
use resumed.v1 across both VMaaS and CaaS. Only initial ""→billable
uses started.v1. Eliminates adapter special-casing between resource
types.

Signed-off-by: omer-vishlitzky <ovishliz@redhat.com>
Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: omer-vishlitzky <omer.vishlitzky@gmail.com>
MapWatchEvent now takes billingDims as a parameter instead of calling
mapper.BillingDimensionsMap() internally. Callers pass the right dims
for their context:

- Audit events (created/deleted): topLevelDims() strips components
  BEFORE building the CloudEvent (was: build with nested, then
  deserialize-strip-reserialize after)
- Lifecycle events: dims passed through, replaced per-component
  during decomposition anyway

Removes stripComponentsFromAuditEvent (serialize-deserialize round-trip).

Signed-off-by: omer-vishlitzky <ovishliz@redhat.com>
Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: omer-vishlitzky <omer.vishlitzky@gmail.com>
…view

ChangedComponents compared only NodeCount and HostType, so a cluster
upgrade (release_image change) or template change with unchanged
topology was silently dropped: no updated.v1 published, and the
projection still absorbed the new dimensions unconditionally in
handleScalingEvent — which permanently defeats the reconciler's
billing_dimensions_drift safety net, since the projection now matches
fulfillment and no drift is ever detected. Fixed by comparing via
DimensionsEqual on FlatBillingDimensions, the same equality already
used to gate entry into this code path, so the two can't disagree.

Two ID-derivation bugs meant retries didn't dedup the way CAP-16
("duplicate events do not cause double-counting") requires:
- Correction event IDs were derived from resourceID/reason/states only,
  with no discriminator. Two distinct billing_dimensions_drift
  corrections for the same resource/state collided on ID, so adapter
  dedup silently dropped the second one. Now includes a content
  fingerprint of the billing dimensions, so distinct drifts get
  distinct IDs while repeat detection of the *same* unresolved drift
  still dedups as designed.
- Reconciler's synthetic heartbeat IDs were keyed off "now", so a retry
  in a later reconciliation cycle for the same unresolved staleness gap
  minted a fresh ID for components already delivered in a prior partial
  failure. Now keyed off LastHeartbeatAt/BillableSince — the actual
  reference point for the gap — so retries of the same gap dedup.

Heartbeat Generator: base ID now keyed to the heartbeat window instead
of a random UUID (consistency/future-proofing for the ID scheme, not
an active bug — each real tick is a legitimately new heartbeat either
way). More importantly, one resource's publish failure no longer
aborts the whole tick; it's isolated so other resources still get
heartbeated.

Also:
- buildComponentEvent guards against a nil base-event data map
  (DataAs leaves the target nil, not an error, when data is empty)
- node_count is validated (integral, non-negative, in int32 range)
  before narrowing; a corrupt value now fails the whole decomposition
  loudly instead of either silently truncating/wrapping the billed
  value or (an earlier draft of this fix) silently dropping just the
  bad component, which would have been a new, differently-shaped
  silent data loss
- Unified the duplicated lifecycle/scaling event payload construction
  (meteringData / buildScalingEvent's hand-rolled map) into one
  exported LifecycleData/BuildLifecycleData used by both, so the two
  producers can't drift into different shapes on the same topic
- Removed the dead StateContext.NewDimensions field (assigned, never read)

Assisted-by: Claude Code <noreply@anthropic.com>
…idation gaps from review

- Bump private-api pin to v0.0.84 and regenerate: fulfillment-service's
  version_name->ClusterVersionReference conversion made metering's stale
  proto silently decode the wrong field. Read spec.GetVersion().GetName().
- Track per-component billable-since (ComponentBillableSince) instead of
  one cluster-wide timestamp, so staggered scaling of independent node
  sets no longer understates duration_seconds for whichever component
  didn't cause the most recent reset. Threaded through Reconciler's
  missed_creation/state_drift paths too, since they write projection rows
  directly. Folded into the initial migration (no live database yet).
- Remove node_count validation from DecomposeClusterComponents entirely:
  fulfillment-service already rejects non-positive node set sizes at the
  API layer, and ClusterBillingDimensions always writes a valid typed
  value, so the check (and JSONB-corruption-only failure mode a stricter
  version of it would have added) defended against a scenario that can't
  occur through any real code path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@masayag

masayag commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

/approve
/lgtm

@openshift-ci

openshift-ci Bot commented Aug 9, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: amito, masayag, omer-vishlitzky

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:
  • OWNERS [amito,masayag,omer-vishlitzky]

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@masayag

masayag commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

/lgtm

@omer-vishlitzky

Copy link
Copy Markdown
Contributor Author

/unhold

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants