Repository navigation
Billing: Contract + Emit Values #2428
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9b19176
9388257
96be6dc
eed474e
48f88a9
4267c91
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -52,6 +52,7 @@ package resourcemanager | |
|
|
||
| import ( | ||
| "context" | ||
| "math/big" | ||
| "strconv" | ||
| "sync" | ||
| "time" | ||
|
|
@@ -444,6 +445,13 @@ func (rm *ResourceManager) EmitUsage(ctx context.Context, identity ResourceIdent | |
| rm.emitRecord(ctx, identity, eventID, meteringpb.MeterAction_METER_ACTION_USAGE, NewUtilizationInt(quantity, fields)) | ||
| } | ||
|
|
||
| // EmitUsageValue is EmitUsage for quantities that do not fit an int64, such as a | ||
| // transaction fee in wei. A nil value emits "0". Same event_id contract and | ||
| // fail-open semantics as EmitUsage. | ||
| func (rm *ResourceManager) EmitUsageValue(ctx context.Context, identity ResourceIdentity, eventID string, value *big.Int, fields UtilizationFields) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. one note is that I'm not sure how the *big.Int will go across the wire in the message payload. it will need to be serialized to string, we should double check that the emitRecord call will handle this properly. we use a cloudEvent packaging, which I believe wraps OTEL |
||
| rm.emitRecord(ctx, identity, eventID, meteringpb.MeterAction_METER_ACTION_USAGE, NewUtilizationBig(value, fields)) | ||
| } | ||
|
|
||
| // emitRecord builds and emits a single-utilization MeterRecord for action. | ||
| // eventID is supplied by the caller and stamped on the utilization; it is never | ||
| // generated here. An empty eventID means the producer failed to derive a | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| package resourcemanager | ||
|
|
||
| import ( | ||
| "errors" | ||
| "strconv" | ||
| "strings" | ||
| ) | ||
|
|
||
| // Workflow capability usage records: one METER_ACTION_USAGE MeterRecord per | ||
| // billable capability event of a workflow execution (compute duration, gas per | ||
| // chain write). The billing service turns each into a billing_records row with | ||
| // event id "cre:workflow:<workflow_id>:<execution_id>:<capability_event_id>", | ||
| // built from Utilization.ResourceId ("<workflow_id>:<execution_id>") and | ||
| // Utilization.EventId (the capability event id). Both must be identical on every | ||
| // node of the DON: they are the quorum and dedup key. | ||
| const ( | ||
| // WorkflowRecordType is the billing record type shared by all workflow | ||
| // capability usage resource types. | ||
| WorkflowRecordType = "cre:workflow" | ||
|
|
||
| // ResourceTypeWorkflowCompute meters ordinary workflow compute in | ||
| // milliseconds. The capability event id is the workflow execution id. | ||
| ResourceTypeWorkflowCompute = WorkflowRecordType + ":compute" | ||
|
|
||
| // ResourceTypeWorkflowGasPrefix is the prefix of the per-chain gas resource | ||
| // type; see WorkflowGasResourceType. Values are in the chain's native | ||
| // smallest unit (wei, lamports). The capability event id is the tx hash. | ||
| ResourceTypeWorkflowGasPrefix = WorkflowRecordType + ":gas:" | ||
|
|
||
| // EmittingServiceChainWrite is the Identity.Service used by chain-write | ||
| // capabilities for gas usage records. | ||
| EmittingServiceChainWrite = "chain-write" | ||
| // EmittingServiceWorkflowEngine is the Identity.Service used by the | ||
| // workflow engine for compute usage records. | ||
| EmittingServiceWorkflowEngine = "workflow-engine" | ||
|
|
||
| // WorkflowGasResourcePool is the Identity.ResourcePool for gas usage | ||
| // records; ResourcePoolID is the fully qualified resource type | ||
| // (cre:workflow:gas:<chain_selector>). See WithWorkflowUsagePool. | ||
| WorkflowGasResourcePool = WorkflowRecordType + ":gas" | ||
| // WorkflowComputeResourcePool is the Identity.ResourcePool for compute | ||
| // usage records; ResourcePoolID is the same value. | ||
| WorkflowComputeResourcePool = ResourceTypeWorkflowCompute | ||
| ) | ||
|
|
||
| var errWorkflowUsageResourceID = errors.New("workflow usage resource id: workflow id and execution id must be non-empty and contain no ':'") | ||
|
|
||
| // WithWorkflowUsagePool returns id with ResourcePool and ResourcePoolID set | ||
| // for a workflow usage resource type, per the billing payload contract: | ||
| // pool is the resource type without its chain selector | ||
| // ("cre:workflow:gas", "cre:workflow:compute") and pool id is the fully | ||
| // qualified resource type. | ||
| func WithWorkflowUsagePool(id ResourceIdentity, resourceType string) ResourceIdentity { | ||
| if strings.HasPrefix(resourceType, ResourceTypeWorkflowGasPrefix) { | ||
| id.ResourcePool = WorkflowGasResourcePool | ||
| } else { | ||
| id.ResourcePool = resourceType | ||
| } | ||
| id.ResourcePoolID = resourceType | ||
| return id | ||
| } | ||
|
|
||
| // WorkflowGasResourceType returns the gas resource type for a chain selector, | ||
| // e.g. "cre:workflow:gas:421614". | ||
| func WorkflowGasResourceType(chainSelector uint64) string { | ||
| return ResourceTypeWorkflowGasPrefix + strconv.FormatUint(chainSelector, 10) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cc @seanwangsmartcontract - calling for viz, but this matches our expectations in https://docs.google.com/document/d/1EQSjpfxNHsBWFikfdVoaIh_KtaPKJQlqaKS8SnpQqfM/edit?tab=t.jlozzrlh0lnu#heading=h.6mpcocqx8ink
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Matches that section: |
||
| } | ||
|
|
||
| // WorkflowUsageResourceID returns the Utilization.ResourceId for a workflow | ||
| // execution: "<workflow_id>:<execution_id>". Neither component may be empty or | ||
| // contain ':', since the consumer splits on the first ':'. | ||
| func WorkflowUsageResourceID(workflowID, executionID string) (string, error) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: is this shared with any other components? maybe deserves its own file or even a "utils" package? |
||
| if workflowID == "" || executionID == "" || strings.Contains(workflowID, ":") || strings.Contains(executionID, ":") { | ||
| return "", errWorkflowUsageResourceID | ||
| } | ||
| return workflowID + ":" + executionID, nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| package resourcemanager | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestWorkflowGasResourceType(t *testing.T) { | ||
| assert.Equal(t, "cre:workflow:gas:421614", WorkflowGasResourceType(421614)) | ||
| assert.Equal(t, "cre:workflow:compute", ResourceTypeWorkflowCompute) | ||
| } | ||
|
|
||
| func TestWorkflowUsageResourceID(t *testing.T) { | ||
| id, err := WorkflowUsageResourceID("wf-1", "exec-1") | ||
| require.NoError(t, err) | ||
| assert.Equal(t, "wf-1:exec-1", id) | ||
|
|
||
| for _, tc := range [][2]string{{"", "exec-1"}, {"wf-1", ""}, {"wf:1", "exec-1"}, {"wf-1", "exec:1"}} { | ||
| _, err := WorkflowUsageResourceID(tc[0], tc[1]) | ||
| assert.ErrorIs(t, err, errWorkflowUsageResourceID, "%q %q", tc[0], tc[1]) | ||
| } | ||
| } | ||
|
|
||
| func TestWithWorkflowUsagePool(t *testing.T) { | ||
| base := ResourceIdentity{Product: "cre", Service: EmittingServiceChainWrite} | ||
| gas := WithWorkflowUsagePool(base, WorkflowGasResourceType(421614)) | ||
| assert.Equal(t, "cre:workflow:gas", gas.ResourcePool) | ||
| assert.Equal(t, "cre:workflow:gas:421614", gas.ResourcePoolID) | ||
| assert.Equal(t, EmittingServiceChainWrite, gas.Service, "other fields untouched") | ||
|
|
||
| compute := WithWorkflowUsagePool(base, ResourceTypeWorkflowCompute) | ||
| assert.Equal(t, "cre:workflow:compute", compute.ResourcePool) | ||
| assert.Equal(t, "cre:workflow:compute", compute.ResourcePoolID) | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.