Repository navigation
NO-ISSUE: fix - preserve billing context through transient states and treat DELETING as billing-ending - #176
Conversation
WalkthroughThe change classifies ChangesCompute Instance State Handling
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant WatchConsumer
participant CloudEventType
participant ProjectionStore
participant CloudEventPublisher
WatchConsumer->>CloudEventType: Resolve compute-instance state
CloudEventType-->>WatchConsumer: Return ErrTransientState
WatchConsumer->>ProjectionStore: Update transient projection metadata
ProjectionStore-->>WatchConsumer: Persist projection
WatchConsumer->>CloudEventPublisher: Publish terminal-state CloudEvent
Possibly related PRs
Suggested reviewers: Caution Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional.
❌ Failed checks (1 error)
✅ Passed checks (10 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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/watch/consumer_test.go`:
- Around line 713-781: The RUNNING→STOPPING→STOPPED test only verifies a
positive duration and does not prove BillableSince was preserved. Update the
test around “preserves billing context through RUNNING→STOPPING→STOPPED
sequence” to use fixed transition timestamps for the original billable start and
STOPPED event, then assert duration_seconds equals the full expected elapsed
duration from the original BillableSince to STOPPED rather than merely being
greater than zero.
In `@osac-metering/metering-service/internal/watch/consumer.go`:
- Around line 153-155: Remove mapper.ResourceID() and any customer resource
identifier from the log fields and error text in the logging paths around
handleTransientState and lines 196-224. Preserve the existing log behavior and
other non-sensitive context while ensuring no customer compute resource IDs are
emitted.
🪄 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: 614ba480-9cd8-49cd-9a02-a33175a2ac06
📒 Files selected for processing (5)
osac-metering/metering-service/internal/events/compute_instance.goosac-metering/metering-service/internal/events/mapper.goosac-metering/metering-service/internal/events/mapper_test.goosac-metering/metering-service/internal/watch/consumer.goosac-metering/metering-service/internal/watch/consumer_test.go
| if errors.Is(err, events.ErrTransientState) { | ||
| return c.handleTransientState(ctx, mapper, existing, version, currentState, transitionTime) | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Remove customer resource identifiers from the new logs.
mapper.ResourceID() identifies the customer compute resource. Lines 216-224 write it to logs. Remove it from the log fields and error text.
Proposed fix
- return c.handleTransientState(ctx, mapper, existing, version, currentState, transitionTime)
+ return c.handleTransientState(ctx, existing, version, currentState, transitionTime)
func (c *Consumer) handleTransientState(
ctx context.Context,
- mapper events.ResourceMapper,
existing *projection.ResourceState,
version int32,
currentState string,
@@
- c.logger.Info("stale version during transient state update, skipping",
- "resource_id", mapper.ResourceID(), "state", currentState)
+ c.logger.Info("stale version during transient state update, skipping",
+ "state", currentState)
return nil
}
- return fmt.Errorf("upserting transient state for %s: %w", mapper.ResourceID(), err)
+ return fmt.Errorf("upserting transient state: %w", err)
}
c.logger.V(1).Info("transient state updated (no CloudEvent)",
- "resource_id", mapper.ResourceID(), "state", currentState)
+ "state", currentState)As per coding guidelines, "Do not log passwords, tokens, API keys, PII ... or customer data."
Also applies to: 196-224
🤖 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 153 -
155, Remove mapper.ResourceID() and any customer resource identifier from the
log fields and error text in the logging paths around handleTransientState and
lines 196-224. Preserve the existing log behavior and other non-sensitive
context while ensuring no customer compute resource IDs are emitted.
Source: Coding guidelines
9745102 to
552cf7c
Compare
552cf7c to
1c20a34
Compare
…TING/DELETING) Bug: When a RUNNING VM is stopped, the state machine goes RUNNING→STOPPING→STOPPED. The Watch Consumer was emitting updated.v1 for STOPPING (with duration_seconds) and suspended.v1 for STOPPED (with null duration_seconds and previous_state=STOPPING). The design says suspended.v1 should carry duration_seconds for billing closure. Root cause: resolveUpdatedEventType mapped STOPPING to updated.v1, causing the projection to overwrite IsBillable=false and BillableSince=nil at the STOPPING step. By the time STOPPED arrived, the billing context was gone. Fix: STOPPING, STARTING, and DELETING are now transient states that return ErrTransientState from CloudEventType. The Watch Consumer catches this error and updates only CurrentState + FulfillmentVersion in the projection — IsBillable and BillableSince are preserved. No CloudEvent is published for transient states. When the final state arrives (STOPPED, RUNNING, DELETED), the billing context is intact: buildStateContext sees IsBillable=true and computes duration_seconds correctly. The suspended.v1 event has previous_state=STOPPING (the actual previous state from the projection) and duration_seconds > 0 (the correct billing interval). Tests: - New: RUNNING→STOPPING→STOPPED two-step sequence through Watch Consumer — verifies no CloudEvent for STOPPING, suspended.v1 has duration_seconds > 0 - Updated: STOPPING/STARTING/DELETING now expect ErrTransientState instead of updated.v1 Assisted-by: Claude Code <noreply@anthropic.com> Signed-off-by: omer-vishlitzky <omer.vishlitzky@gmail.com>
1c20a34 to
2017848
Compare
DELETING emits suspended.v1 to close the billing interval. Unlike STOPPING (which always leads to STOPPED where suspended.v1 fires), DELETING leads to OBJECT_DELETED (deleted.v1) which is audit-only. Making DELETING transient would lose billing closure for direct deletes (RUNNING → DELETING → DELETED). Also strengthens the RUNNING→STOPPING→STOPPED test to use fixed timestamps and assert exact duration_seconds (3600s), catching any corruption of BillableSince during transient state handling. Signed-off-by: omer-vishlitzky <ovishliz@redhat.com> Assisted-by: Claude Code <noreply@anthropic.com> Signed-off-by: omer-vishlitzky <omer.vishlitzky@gmail.com>
|
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. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/mapper_test.go`:
- Around line 259-271: Update the unknown-state test around mapEvent to set
ci.PreviousState to a known state, then assert the returned error contains both
the previous state and the unknown current state values in addition to the
existing transition message. Keep the ErrTransientState assertion unchanged.
🪄 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: f8ad3806-c423-46ee-a988-763bc992fdf3
📒 Files selected for processing (6)
osac-metering/metering-service/internal/events/compute_instance.goosac-metering/metering-service/internal/events/mapper.goosac-metering/metering-service/internal/events/mapper_test.goosac-metering/metering-service/internal/reconciliation/correction.goosac-metering/metering-service/internal/watch/consumer.goosac-metering/metering-service/internal/watch/consumer_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
- osac-metering/metering-service/internal/events/mapper.go
- osac-metering/metering-service/internal/watch/consumer_test.go
- osac-metering/metering-service/internal/events/compute_instance.go
- osac-metering/metering-service/internal/watch/consumer.go
| It("returns error for unknown state (default branch)", func() { | ||
| ci.Status.State = privatev1.ComputeInstanceState(9999) | ||
|
|
||
| event := &privatev1.Event{ | ||
| Id: "evt-unspecified", | ||
| Id: "evt-unknown-state", | ||
| Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, | ||
| Payload: &privatev1.Event_ComputeInstance{ComputeInstance: ci}, | ||
| } | ||
|
|
||
| ce, err := mapEvent(event, &events.StateContext{}) | ||
| Expect(err).NotTo(HaveOccurred()) | ||
| Expect(ce.Type()).To(Equal("osac.resource.updated.v1")) | ||
| _, err := mapEvent(event, &events.StateContext{}) | ||
| Expect(err).To(HaveOccurred()) | ||
| Expect(err.Error()).To(ContainSubstring("unexpected compute instance state transition")) | ||
| Expect(errors.Is(err, events.ErrTransientState)).To(BeFalse()) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Assert the complete unknown transition.
The test checks only the static error prefix. A generic error without the previous or unknown current state would still pass. Set a known PreviousState and assert that the error includes both transition values.
🤖 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/mapper_test.go` around lines
259 - 271, Update the unknown-state test around mapEvent to set ci.PreviousState
to a known state, then assert the returned error contains both the previous
state and the unknown current state values in addition to the existing
transition message. Keep the ErrTransientState assertion unchanged.
handleTransientState now updates only FulfillmentVersion and TransitionTime. CurrentState stays as-is (e.g., RUNNING) so when the final state arrives (STOPPED), previous_state=RUNNING on suspended.v1 — matching the design exactly. Signed-off-by: omer-vishlitzky <ovishliz@redhat.com> Assisted-by: Claude Code <noreply@anthropic.com> Signed-off-by: omer-vishlitzky <omer.vishlitzky@gmail.com>
|
@omer-vishlitzky: This pull request explicitly references no jira issue. DetailsIn response to this:
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. |
|
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: masayag, omer-vishlitzky The full list of commands accepted by this bot can be found here. The pull request process is described here DetailsNeeds approval from an approver in each of these files:
Approvers can indicate their approval by writing |
|
/lgtm |
OSAC-4497: Admin bare metal instance type create and edit page
Summary
Fixes a billing data integrity bug where transient states (STOPPING,
STARTING) destroyed billing context, causing
suspended.v1events tohave
nullduration_secondsand wrongprevious_state.Bug
When a RUNNING VM is stopped, the state machine goes:
RUNNING → STOPPING → STOPPED
The Watch Consumer emitted:
updated.v1for STOPPING — overwroteIsBillable=false,BillableSince=nilsuspended.v1for STOPPED —duration_seconds=null,previous_state=STOPPINGThe design doc (line 341) says
suspended.v1should carryduration_secondsfor billing closure. Adapters watching forsuspended.v1to close billing intervals got no duration.Root cause
resolveUpdatedEventTypemapped STOPPING toupdated.v1. Theprojection update overwrote
IsBillable=falseandBillableSince=nil.When STOPPED arrived, billing context was gone.
Fix
STOPPING and STARTING now return
ErrTransientState. The WatchConsumer catches this and updates only
FulfillmentVersion+TransitionTimein the projection —CurrentState,IsBillable,and
BillableSinceare all preserved. No CloudEvent is published.When the final state arrives (STOPPED), the projection still shows
CurrentState=RUNNING, sosuspended.v1hasprevious_state=RUNNINGand correct
duration_seconds. This matches the design exactly.DELETING emits
suspended.v1(billing closure), same asSTOPPED/PAUSED/FAILED — it is a billing-ending state, not transient.
Unknown states return an error (fail fast). All supported state
transitions are explicit in the switch.
Before/After
updated.v1(destroyed billing context)suspended.v1(duration=null, prev=STOPPING)suspended.v1(duration=3600, prev=RUNNING)updated.v1(consumed billing context)suspended.v1(duration=3600, billing closed)updated.v1(silent)Test plan
ginkgo run -r internal— 127 specs passsuspended.v1hasprevious_state=RUNNINGandduration_seconds ≈ 3600(fixed timestamps prove BillableSince preserved, not reset)
ErrTransientStatesuspended.v1ErrTransientStateReferences
suspended.v1— Billing interval closes with exactduration_seconds"osac.resource.suspended.v1"Assisted-by: Claude Code noreply@anthropic.com