Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions osac-metering/metering-service/internal/database/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,13 +194,6 @@ func (i *Instance) init(ctx context.Context) error {
return nil
}

func (i *Instance) URL(ctx context.Context) (string, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this related to the changes introduced in this PR?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

no, just dead code

if err := i.init(ctx); err != nil {
return "", err
}
return i.url, nil
}

func (i *Instance) Pool(ctx context.Context) (*pgxpool.Pool, error) {
if err := i.init(ctx); err != nil {
return nil, err
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ CREATE TABLE metering_resource_state (
project_id TEXT,
current_state TEXT NOT NULL,
previous_state TEXT,
is_billable BOOLEAN NOT NULL DEFAULT FALSE,
billable_since TIMESTAMPTZ,
is_billable BOOLEAN GENERATED ALWAYS AS (billable_since IS NOT NULL) STORED,
ever_billable BOOLEAN NOT NULL DEFAULT FALSE,
last_heartbeat_at TIMESTAMPTZ,
transition_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
fulfillment_version INT NOT NULL,
Expand Down
25 changes: 13 additions & 12 deletions osac-metering/metering-service/internal/events/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,25 +103,26 @@ func (m *clusterMapper) BillingDimensionsMap() map[string]any {
// the Watch Consumer via DimensionsEqual; the hourly reconciler catches
// any missed dimension drift.
var clusterTransitions = TransitionTable{
// Started: first billable state (no previous)
{StateEmpty, ClusterStateProgressing}: {EventType: EventStarted},
{StateEmpty, ClusterStateReady}: {EventType: EventStarted},
// Crossed into billable: label (started.v1 vs resumed.v1) is resolved by
// MapWatchEvent from StateContext.EverBillable, not by which previous
// state this row matched -- see eventBillableStart's doc comment.
{StateEmpty, ClusterStateProgressing}: {EventType: eventBillableStart},
{StateEmpty, ClusterStateReady}: {EventType: eventBillableStart},

// Skip: first observed in non-billable state (bootstrap, reconnect after failure)
{StateEmpty, ClusterStateFailed}: {Skip: true},
{StateEmpty, ClusterStateDeleting}: {Skip: true},
{StateEmpty, ClusterStateDeleteFailed}: {Skip: true},
{StateEmpty, ClusterStateUnspecified}: {Skip: true},

// Resumed: non-billable to billable
{ClusterStateFailed, ClusterStateProgressing}: {EventType: EventResumed},
{ClusterStateFailed, ClusterStateReady}: {EventType: EventResumed},
{ClusterStateDeleting, ClusterStateProgressing}: {EventType: EventResumed},
{ClusterStateDeleting, ClusterStateReady}: {EventType: EventResumed},
{ClusterStateDeleteFailed, ClusterStateProgressing}: {EventType: EventResumed},
{ClusterStateDeleteFailed, ClusterStateReady}: {EventType: EventResumed},
{ClusterStateUnspecified, ClusterStateProgressing}: {EventType: EventResumed},
{ClusterStateUnspecified, ClusterStateReady}: {EventType: EventResumed},
{ClusterStateFailed, ClusterStateProgressing}: {EventType: eventBillableStart},
{ClusterStateFailed, ClusterStateReady}: {EventType: eventBillableStart},
{ClusterStateDeleting, ClusterStateProgressing}: {EventType: eventBillableStart},
{ClusterStateDeleting, ClusterStateReady}: {EventType: eventBillableStart},
{ClusterStateDeleteFailed, ClusterStateProgressing}: {EventType: eventBillableStart},
{ClusterStateDeleteFailed, ClusterStateReady}: {EventType: eventBillableStart},
{ClusterStateUnspecified, ClusterStateProgressing}: {EventType: eventBillableStart},
{ClusterStateUnspecified, ClusterStateReady}: {EventType: eventBillableStart},

// Suspended: billable to non-billable
{ClusterStateProgressing, ClusterStateFailed}: {EventType: EventSuspended},
Expand Down
138 changes: 79 additions & 59 deletions osac-metering/metering-service/internal/events/cluster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ var _ = Describe("CaaS Cluster Mapper", func() {

Context("state machine — full transition matrix", func() {
DescribeTable("resolves correct CloudEvent type for state transitions",
func(currentState privatev1.ClusterState, previousState string, expectedType string, expectSkip bool) {
func(currentState privatev1.ClusterState, previousState string, everBillable bool, expectedType string, expectSkip bool) {
cl.Status.State = currentState

event := &privatev1.Event{
Expand All @@ -53,7 +53,7 @@ var _ = Describe("CaaS Cluster Mapper", func() {
Payload: &privatev1.Event_Cluster{Cluster: cl},
}

stateCtx := &events.StateContext{PreviousState: previousState}
stateCtx := &events.StateContext{PreviousState: previousState, EverBillable: everBillable}
ce, err := mapEvent(event, stateCtx)

if expectSkip {
Expand All @@ -64,103 +64,123 @@ var _ = Describe("CaaS Cluster Mapper", func() {
Expect(ce.Type()).To(Equal(expectedType))
}
},
// --- Started: first billable state (no previous) ---
Entry("initial PROGRESSING (prev=empty) -> started.v1",
privatev1.ClusterState_CLUSTER_STATE_PROGRESSING, events.StateEmpty, events.EventStarted, false),
Entry("initial READY (prev=empty) -> started.v1",
privatev1.ClusterState_CLUSTER_STATE_READY, events.StateEmpty, events.EventStarted, false),
// --- Crossed into billable, from "" (existing==nil, so EverBillable
// is necessarily false -- only fires on missed-CREATE recovery) ---
Entry("initial PROGRESSING (prev=empty), never billable before -> started.v1",
privatev1.ClusterState_CLUSTER_STATE_PROGRESSING, events.StateEmpty, false, events.EventStarted, false),
Entry("initial READY (prev=empty), never billable before -> started.v1",
privatev1.ClusterState_CLUSTER_STATE_READY, events.StateEmpty, false, events.EventStarted, false),

// --- Skip: first observed in non-billable state ---
Entry("initial FAILED (prev=empty) -> skip",
privatev1.ClusterState_CLUSTER_STATE_FAILED, events.StateEmpty, "", true),
privatev1.ClusterState_CLUSTER_STATE_FAILED, events.StateEmpty, false, "", true),
Entry("initial DELETING (prev=empty) -> skip",
privatev1.ClusterState_CLUSTER_STATE_DELETING, events.StateEmpty, "", true),
privatev1.ClusterState_CLUSTER_STATE_DELETING, events.StateEmpty, false, "", true),
Entry("initial DELETE_FAILED (prev=empty) -> skip",
privatev1.ClusterState_CLUSTER_STATE_DELETE_FAILED, events.StateEmpty, "", true),
privatev1.ClusterState_CLUSTER_STATE_DELETE_FAILED, events.StateEmpty, false, "", true),
Entry("initial UNSPECIFIED (prev=empty) -> skip",
privatev1.ClusterState_CLUSTER_STATE_UNSPECIFIED, events.StateEmpty, "", true),

// --- Resumed: non-billable to billable ---
Entry("FAILED -> PROGRESSING -> resumed.v1",
privatev1.ClusterState_CLUSTER_STATE_PROGRESSING, events.ClusterStateFailed, events.EventResumed, false),
Entry("FAILED -> READY -> resumed.v1",
privatev1.ClusterState_CLUSTER_STATE_READY, events.ClusterStateFailed, events.EventResumed, false),
Entry("DELETING -> PROGRESSING -> resumed.v1",
privatev1.ClusterState_CLUSTER_STATE_PROGRESSING, events.ClusterStateDeleting, events.EventResumed, false),
Entry("DELETING -> READY -> resumed.v1",
privatev1.ClusterState_CLUSTER_STATE_READY, events.ClusterStateDeleting, events.EventResumed, false),
Entry("DELETE_FAILED -> PROGRESSING -> resumed.v1",
privatev1.ClusterState_CLUSTER_STATE_PROGRESSING, events.ClusterStateDeleteFailed, events.EventResumed, false),
Entry("DELETE_FAILED -> READY -> resumed.v1",
privatev1.ClusterState_CLUSTER_STATE_READY, events.ClusterStateDeleteFailed, events.EventResumed, false),
Entry("UNSPECIFIED -> PROGRESSING -> resumed.v1",
privatev1.ClusterState_CLUSTER_STATE_PROGRESSING, events.ClusterStateUnspecified, events.EventResumed, false),
Entry("UNSPECIFIED -> READY -> resumed.v1",
privatev1.ClusterState_CLUSTER_STATE_READY, events.ClusterStateUnspecified, events.EventResumed, false),

// --- Suspended: billable to non-billable ---
privatev1.ClusterState_CLUSTER_STATE_UNSPECIFIED, events.StateEmpty, false, "", true),

// --- Crossed into billable, from a real prior state: label depends on
// EverBillable, not on which state matched -- both variants proven for
// each reachable previousState (UNSPECIFIED is the real-world case: a
// fresh cluster whose CREATE is observed before status is populated) ---
Entry("FAILED -> PROGRESSING, billable before -> resumed.v1",
privatev1.ClusterState_CLUSTER_STATE_PROGRESSING, events.ClusterStateFailed, true, events.EventResumed, false),
Entry("FAILED -> PROGRESSING, never billable before -> started.v1",
privatev1.ClusterState_CLUSTER_STATE_PROGRESSING, events.ClusterStateFailed, false, events.EventStarted, false),
Entry("FAILED -> READY, billable before -> resumed.v1",
privatev1.ClusterState_CLUSTER_STATE_READY, events.ClusterStateFailed, true, events.EventResumed, false),
Entry("FAILED -> READY, never billable before -> started.v1",
privatev1.ClusterState_CLUSTER_STATE_READY, events.ClusterStateFailed, false, events.EventStarted, false),
Entry("DELETING -> PROGRESSING, billable before -> resumed.v1",
privatev1.ClusterState_CLUSTER_STATE_PROGRESSING, events.ClusterStateDeleting, true, events.EventResumed, false),
Entry("DELETING -> PROGRESSING, never billable before -> started.v1",
privatev1.ClusterState_CLUSTER_STATE_PROGRESSING, events.ClusterStateDeleting, false, events.EventStarted, false),
Entry("DELETING -> READY, billable before -> resumed.v1",
privatev1.ClusterState_CLUSTER_STATE_READY, events.ClusterStateDeleting, true, events.EventResumed, false),
Entry("DELETING -> READY, never billable before -> started.v1",
privatev1.ClusterState_CLUSTER_STATE_READY, events.ClusterStateDeleting, false, events.EventStarted, false),
Entry("DELETE_FAILED -> PROGRESSING, billable before -> resumed.v1",
privatev1.ClusterState_CLUSTER_STATE_PROGRESSING, events.ClusterStateDeleteFailed, true, events.EventResumed, false),
Entry("DELETE_FAILED -> PROGRESSING, never billable before -> started.v1",
privatev1.ClusterState_CLUSTER_STATE_PROGRESSING, events.ClusterStateDeleteFailed, false, events.EventStarted, false),
Entry("DELETE_FAILED -> READY, billable before -> resumed.v1",
privatev1.ClusterState_CLUSTER_STATE_READY, events.ClusterStateDeleteFailed, true, events.EventResumed, false),
Entry("DELETE_FAILED -> READY, never billable before -> started.v1",
privatev1.ClusterState_CLUSTER_STATE_READY, events.ClusterStateDeleteFailed, false, events.EventStarted, false),
Entry("UNSPECIFIED -> PROGRESSING, billable before -> resumed.v1",
privatev1.ClusterState_CLUSTER_STATE_PROGRESSING, events.ClusterStateUnspecified, true, events.EventResumed, false),
Entry("UNSPECIFIED -> PROGRESSING, never billable before -> started.v1",
privatev1.ClusterState_CLUSTER_STATE_PROGRESSING, events.ClusterStateUnspecified, false, events.EventStarted, false),
Entry("UNSPECIFIED -> READY, billable before -> resumed.v1",
privatev1.ClusterState_CLUSTER_STATE_READY, events.ClusterStateUnspecified, true, events.EventResumed, false),
Entry("UNSPECIFIED -> READY, never billable before -> started.v1",
privatev1.ClusterState_CLUSTER_STATE_READY, events.ClusterStateUnspecified, false, events.EventStarted, false),

// --- Suspended: billable to non-billable (EverBillable irrelevant) ---
Entry("PROGRESSING -> FAILED -> suspended.v1",
privatev1.ClusterState_CLUSTER_STATE_FAILED, events.ClusterStateProgressing, events.EventSuspended, false),
privatev1.ClusterState_CLUSTER_STATE_FAILED, events.ClusterStateProgressing, false, events.EventSuspended, false),
Entry("PROGRESSING -> DELETING -> suspended.v1",
privatev1.ClusterState_CLUSTER_STATE_DELETING, events.ClusterStateProgressing, events.EventSuspended, false),
privatev1.ClusterState_CLUSTER_STATE_DELETING, events.ClusterStateProgressing, false, events.EventSuspended, false),
Entry("READY -> FAILED -> suspended.v1",
privatev1.ClusterState_CLUSTER_STATE_FAILED, events.ClusterStateReady, events.EventSuspended, false),
privatev1.ClusterState_CLUSTER_STATE_FAILED, events.ClusterStateReady, false, events.EventSuspended, false),
Entry("READY -> DELETING -> suspended.v1",
privatev1.ClusterState_CLUSTER_STATE_DELETING, events.ClusterStateReady, events.EventSuspended, false),
privatev1.ClusterState_CLUSTER_STATE_DELETING, events.ClusterStateReady, false, events.EventSuspended, false),
Entry("PROGRESSING -> UNSPECIFIED -> suspended.v1",
privatev1.ClusterState_CLUSTER_STATE_UNSPECIFIED, events.ClusterStateProgressing, events.EventSuspended, false),
privatev1.ClusterState_CLUSTER_STATE_UNSPECIFIED, events.ClusterStateProgressing, false, events.EventSuspended, false),
Entry("READY -> UNSPECIFIED -> suspended.v1",
privatev1.ClusterState_CLUSTER_STATE_UNSPECIFIED, events.ClusterStateReady, events.EventSuspended, false),
privatev1.ClusterState_CLUSTER_STATE_UNSPECIFIED, events.ClusterStateReady, false, events.EventSuspended, false),
Entry("READY -> DELETE_FAILED -> suspended.v1",
privatev1.ClusterState_CLUSTER_STATE_DELETE_FAILED, events.ClusterStateReady, events.EventSuspended, false),
privatev1.ClusterState_CLUSTER_STATE_DELETE_FAILED, events.ClusterStateReady, false, events.EventSuspended, false),
Entry("PROGRESSING -> DELETE_FAILED -> suspended.v1",
privatev1.ClusterState_CLUSTER_STATE_DELETE_FAILED, events.ClusterStateProgressing, events.EventSuspended, false),
privatev1.ClusterState_CLUSTER_STATE_DELETE_FAILED, events.ClusterStateProgressing, false, events.EventSuspended, false),

// --- Skip: billable to billable (no billing boundary) ---
Entry("PROGRESSING -> READY -> skip",
privatev1.ClusterState_CLUSTER_STATE_READY, events.ClusterStateProgressing, "", true),
privatev1.ClusterState_CLUSTER_STATE_READY, events.ClusterStateProgressing, false, "", true),
Entry("READY -> PROGRESSING -> skip",
privatev1.ClusterState_CLUSTER_STATE_PROGRESSING, events.ClusterStateReady, "", true),
privatev1.ClusterState_CLUSTER_STATE_PROGRESSING, events.ClusterStateReady, false, "", true),
Entry("PROGRESSING -> PROGRESSING -> skip (same-state)",
privatev1.ClusterState_CLUSTER_STATE_PROGRESSING, events.ClusterStateProgressing, "", true),
privatev1.ClusterState_CLUSTER_STATE_PROGRESSING, events.ClusterStateProgressing, false, "", true),
Entry("READY -> READY -> skip (same-state, scaling)",
privatev1.ClusterState_CLUSTER_STATE_READY, events.ClusterStateReady, "", true),
privatev1.ClusterState_CLUSTER_STATE_READY, events.ClusterStateReady, false, "", true),

// --- Skip: non-billable same-state ---
Entry("FAILED -> FAILED -> skip (same-state)",
privatev1.ClusterState_CLUSTER_STATE_FAILED, events.ClusterStateFailed, "", true),
privatev1.ClusterState_CLUSTER_STATE_FAILED, events.ClusterStateFailed, false, "", true),
Entry("DELETING -> DELETING -> skip (same-state)",
privatev1.ClusterState_CLUSTER_STATE_DELETING, events.ClusterStateDeleting, "", true),
privatev1.ClusterState_CLUSTER_STATE_DELETING, events.ClusterStateDeleting, false, "", true),
Entry("DELETE_FAILED -> DELETE_FAILED -> skip (same-state)",
privatev1.ClusterState_CLUSTER_STATE_DELETE_FAILED, events.ClusterStateDeleteFailed, "", true),
privatev1.ClusterState_CLUSTER_STATE_DELETE_FAILED, events.ClusterStateDeleteFailed, false, "", true),
Entry("UNSPECIFIED -> UNSPECIFIED -> skip (same-state)",
privatev1.ClusterState_CLUSTER_STATE_UNSPECIFIED, events.ClusterStateUnspecified, "", true),
privatev1.ClusterState_CLUSTER_STATE_UNSPECIFIED, events.ClusterStateUnspecified, false, "", true),

// --- Skip: non-billable to non-billable (cross-state) ---
Entry("FAILED -> DELETING -> skip",
privatev1.ClusterState_CLUSTER_STATE_DELETING, events.ClusterStateFailed, "", true),
privatev1.ClusterState_CLUSTER_STATE_DELETING, events.ClusterStateFailed, false, "", true),
Entry("FAILED -> DELETE_FAILED -> skip",
privatev1.ClusterState_CLUSTER_STATE_DELETE_FAILED, events.ClusterStateFailed, "", true),
privatev1.ClusterState_CLUSTER_STATE_DELETE_FAILED, events.ClusterStateFailed, false, "", true),
Entry("DELETING -> DELETE_FAILED -> skip",
privatev1.ClusterState_CLUSTER_STATE_DELETE_FAILED, events.ClusterStateDeleting, "", true),
privatev1.ClusterState_CLUSTER_STATE_DELETE_FAILED, events.ClusterStateDeleting, false, "", true),
Entry("DELETE_FAILED -> DELETING -> skip",
privatev1.ClusterState_CLUSTER_STATE_DELETING, events.ClusterStateDeleteFailed, "", true),
privatev1.ClusterState_CLUSTER_STATE_DELETING, events.ClusterStateDeleteFailed, false, "", true),
Entry("DELETING -> FAILED -> skip",
privatev1.ClusterState_CLUSTER_STATE_FAILED, events.ClusterStateDeleting, "", true),
privatev1.ClusterState_CLUSTER_STATE_FAILED, events.ClusterStateDeleting, false, "", true),
Entry("DELETE_FAILED -> FAILED -> skip",
privatev1.ClusterState_CLUSTER_STATE_FAILED, events.ClusterStateDeleteFailed, "", true),
privatev1.ClusterState_CLUSTER_STATE_FAILED, events.ClusterStateDeleteFailed, false, "", true),
Entry("UNSPECIFIED -> FAILED -> skip",
privatev1.ClusterState_CLUSTER_STATE_FAILED, events.ClusterStateUnspecified, "", true),
privatev1.ClusterState_CLUSTER_STATE_FAILED, events.ClusterStateUnspecified, false, "", true),
Entry("UNSPECIFIED -> DELETING -> skip",
privatev1.ClusterState_CLUSTER_STATE_DELETING, events.ClusterStateUnspecified, "", true),
privatev1.ClusterState_CLUSTER_STATE_DELETING, events.ClusterStateUnspecified, false, "", true),
Entry("UNSPECIFIED -> DELETE_FAILED -> skip",
privatev1.ClusterState_CLUSTER_STATE_DELETE_FAILED, events.ClusterStateUnspecified, "", true),
privatev1.ClusterState_CLUSTER_STATE_DELETE_FAILED, events.ClusterStateUnspecified, false, "", true),
Entry("FAILED -> UNSPECIFIED -> skip",
privatev1.ClusterState_CLUSTER_STATE_UNSPECIFIED, events.ClusterStateFailed, "", true),
privatev1.ClusterState_CLUSTER_STATE_UNSPECIFIED, events.ClusterStateFailed, false, "", true),
Entry("DELETING -> UNSPECIFIED -> skip",
privatev1.ClusterState_CLUSTER_STATE_UNSPECIFIED, events.ClusterStateDeleting, "", true),
privatev1.ClusterState_CLUSTER_STATE_UNSPECIFIED, events.ClusterStateDeleting, false, "", true),
Entry("DELETE_FAILED -> UNSPECIFIED -> skip",
privatev1.ClusterState_CLUSTER_STATE_UNSPECIFIED, events.ClusterStateDeleteFailed, "", true),
privatev1.ClusterState_CLUSTER_STATE_UNSPECIFIED, events.ClusterStateDeleteFailed, false, "", true),
)

It("returns error for unknown state (missing table entry)", func() {
Expand Down
Loading
Loading