Skip to content

adding resource manager for workflows - #22845

Open
patrickhuie19 wants to merge 18 commits into
developfrom
feat/shared-2712
Open

adding resource manager for workflows#22845
patrickhuie19 wants to merge 18 commits into
developfrom
feat/shared-2712

Conversation

@patrickhuie19

@patrickhuie19 patrickhuie19 commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Durable resource metering for workflow specs (workflow-syncer-v2 × ResourceManager)

SHARED-2712

Requires


TL;DR

This PR makes the v2 workflow registry syncer emit billing-grade MeterRecord deltas for workflow-spec storage: +1 when a workflow's artifacts are first persisted, -1 when they are deleted. Pause and Activate do not produce MeterRecords. To ensure no drift from the deltas (i.e. think of register -> delete -> re-register) we change the ArtifactStorageORM to add a registered_at field to the table, sourced from the CreatedAt uint64 on the smartcontract and centralized registry sources. To preserve the previous functionality we still clear out the workflow and config fields. This additional state prevents us from emitting the same eventID within a dedup window on the billing service.


1. What is being metered

One metered unit = one persisted registration generation in workflow_specs_v2. That row is the node's durable copy of the workflow: the workflow column holds the complete hex-encoded wasm binary and config holds its config (the on-disk module cache and in-memory module LRU are rebuildable caches on top of it).

Identity on every record: service=workflow-syncer-v2, resource_pool=workflow_specs_v2, resource_type=operations, resource_id=workflow_id, plus deployment dimensions (product/tenant/environment/zone/node) from the new [Metering] config and don_id resolved from the DON notifier. Org ID is resolved fail-open from the workflow owner at emit time.

2. Constraints

  1. Every workflow-DON node emits every record. Deltas are deduplicated downstream by event_id, so all nodes fielding the same logical transition must derive byte-identical event_ids from DON-consistent inputs only (resourcemanager.EventID). Any locally-derived discriminator (wall clock, block head at tick time, per-node counters) silently breaks dedup and double-counts.
  2. The consumer's dedup window is hours-to-days and is NOT load-bearing. At steady state there will be hundreds of millions of MeterRecords and workflows live for years; an unbounded exact-once KV is infeasible. Therefore: two different logical transitions must never share an event_id (the later one would be dropped inside the window), and drift must not be "corrected later".
  3. The chain gives us no per-occurrence discriminator for pause/activate. In WorkflowRegistry.sol, _applyPause/_applyActivate mutate only status; createdAt is set once at registration and never changes. The metadata view the syncer reconciles against has no updatedAt, no nonce, no status-change block.
  4. The syncer cannot distinguish a registration from an activation. Reconciliation is state-based; every active-workflow-without-engine becomes a WorkflowActivated event (see the comment in generateReconciliationEvents: "we can't tell the difference between an activation and registration without holding state in the db").

Given the above constraints, if pause deleted the row and emitted -1, the subsequent activation would re-persist the row through the create path and be forced to reuse the register event_id. (wfID, on-chain createdAt) is the only DON-consistent material available, and it's identical to the original registration's. The consumer would then drop that +1 as a duplicate whenever register → pause → activate happen within one dedup window:

There is no id scheme that escapes this without new state. So we hold state: the row survives pause, and deltas are emitted only at generation boundaries, where each fires at most once per (workflow_id, registered_at) and ids never collide across generations. Zero drift by construction, not by reconciliation.

3. Flows

stateDiagram-v2
    direction LR
    [*] --> Absent
    Absent --> Active: register / activate — fetch + INSERT — emit +1
    Active --> Tombstone: pause — one UPDATE sets status=paused and clears binary+config — no delta
    Tombstone --> Active: activate — refetch + UPSERT via existing create path — no delta
    Active --> Absent: delete or orphan sweep — DELETE — emit -1
    Tombstone --> Absent: delete or orphan sweep — DELETE — emit -1
    Active --> Active: status-only flip or redelivered event — no delta
    Tombstone --> Tombstone: redelivered pause — no-op
    Absent --> Absent: redelivered pause / delete — no-op, no delta
Loading

Event_ids

Transition Storage effect Delta event_id
First persistence of a generation (register, or activate with no row, or artifact-mismatch re-persist) INSERT full row incl. registered_at +1 EventID("workflow-spec-register", wfID, onChainCreatedAt)
Pause row kept; status='paused', workflow='', config='' (one statement) none
Activate-after-pause refetch artifacts, full UPSERT, status='active' none
Status-only flip status UPSERT none
Delete (event path or orphan sweep) DELETE … RETURNING registered_at −1 iff a row was removed EventID("workflow-spec-delete", wfID, registered_at) — timestamp part omitted when 0 (pre-migration rows)

registered_at is a new column (migration 0302) persisting the on-chain registration timestamp. the generation discriminator. Without it, a delete → re-register (same content ⇒ same workflowID) → delete sequence inside one dedup window would collide the two -1s.

4. ArtifactStorage Lifecycle Change

Prior to this change workflowPausedEvent delegates to the delete path. The entire row in workflow_specs_v2 is removed, and activation later refetches the binary/config from the storage service and re-inserts it.

Now: pause = drain engine (unchanged ErrDrainInProgress semantics) → stop engine → single UPDATE workflow_specs_v2 SET status='paused', workflow='', config='', updated_at=now() → module-cache cleanup → engine-registry pop. Same step order as the delete path; only the storage operation differs.

I considered keeping the binary/config, but decided against it:

  • Storage is still freed. The hex-encoded binary is the heavy payload in the row (2× binary size as text); the tombstone keeps only identity columns. What the tombstone buys is state: it is the only thing that lets the handler distinguish "activate after pause" (no delta) from "first registration"
  • Activation is unchanged in kind. Activate-after-pause detects the tombstone (spec.Workflow == "" — an invariant, since createWorkflowSpec always persists a non-empty hex binary; commented in code) and runs the existing createWorkflowSpec fetch/upsert path — the exact same storage-service fetch it performs today, minus the delta. Same failure mode on fetch errors (event retried, engine not started).
  • It mirrors the chain. The on-chain registry retains paused registrations; local durable state now has the same shape (same workflow set, same statuses).

5. DB changes

  • 0302_workflow_specs_v2_registered_at.sql: ADD COLUMN registered_at bigint NOT NULL DEFAULT 0.
  • Pre-migration rows carry 0 → their delete-id falls back to the timestamp-less form. Backfill is opportunistic: any event that touches an existing row with registered_at = 0 writes the payload's CreatedAt. The first post-upgrade boot delivers WorkflowActivated for every engine-less active workflow, so fleets converge in one reconciliation pass — before metering can emit anything, because [Metering].MeterRecordsEnabled defaults to false and enabling it requires a restart.

6. Testing

Storage state machine — Test_specStorage_StateMachine. Table-driven over (row state: absent | active | paused-tombstone | paused-with-artifacts) × (event: Registered(active), Registered(paused), Activated, Paused, Deleted — each delivered twice). After every step it asserts: row existence, status, payload emptiness, registered_at, engine-registry contents, artifact-fetch call count, returned error.

Metering lifecycle — handler_metering_test.go (reworked).

  • Test_meterRecords_FullLifecycleEmitsExactlyTwoRecords: register(+1) → pause(0) → activate(0) → delete(−1); asserts exactly two records with the exact event_ids, registered_at flowing insert → RETURNING.
  • Test_meterRecords_PauseActivateCycleIsLevelNeutral; redelivered delete emits exactly one −1; drain-deferred delete emits nothing until the successful retry; Test_meterRecords_TransientDeleteErrorThenRetryEmitsOnce (the lost-−1 bug); Test_meterRecords_LegacyRowDeleteUsesFallbackID; reprocessed register yields the identical event_id; org-ID and don_id stamping; disabled ResourceManager emits nothing; emitter failures never fail event handling.
  • Test_meterRecords_FailOpenEquivalence: identical event sequences with the ResourceManager nil / disabled / erroring produce byte-identical row and engine outcomes and zero handler errors — metering cannot alter deployment behavior by construction.
  • Test_meterRecords_SweepReleasesPausedTombstone: deleted-while-paused through the sweep → exactly one −1 with the generation id.

Source parity — Test_handler_SourceParity_PauseActivateCycle. The full register → pause → activate → delete cycle run twice, once with a contract-source identifier and once with a centralized-registry (gRPC) identifier, asserting identical storage effects, engine-registry source scoping, and identical metering records — pinning that mainline and centralized flows cannot diverge.

Sweep — Test_reconcileOrphanedSpecs_ReleasesEnginelessOrphansOnly (+ engine-present orphans skipped; zero Handle dispatches).

Deliberately flipped assertions (the complete list — everything else in the existing syncer suite passes unchanged): Test_workflowDeletedHandler and drain tests move to the 2-arg workflowDeletedEvent signature; tests asserting "pause deletes the row" now assert "pause tombstones the row"; ORM/store delete tests assert (registeredAt, deleted, err).

End-to-end: local CRE environment (this repo's tooling, metering-enabled TOML in the DON configs) — register → pause → activate → delete on both sources; chip-ingress shows exactly one +1 and one -1 per generation with event_ids identical across all four workflow nodes; engines healthy throughout; during the paused interval the row is present with an empty workflow column

Reviewer guide

Diff area What to look at
core/services/workflows/syncer/v2/handler.go workflowPausedEvent (the behavior change), workflowRegisteredEvent restore branch, releaseSpecStorage, stopEngine extraction, emitSpecDelta
core/services/workflows/syncer/v2/workflow_registry.go Orphan sweep (reconcileOrphanedSpecs, ReleaseOrphanedSpec on evtHandler)
core/services/workflows/artifacts/v2/{orm,store}.go, migration 0302 registered_at, DELETE … RETURNING, PauseWorkflowSpec
core/config/**, core/services/chainlink/**, plugins/loop_registry.go [Metering] config plumbing + LOOP env propagation
core/services/cre/cre.go ResourceManager construction + identity wiring
Test files start with Test_specStorage_StateMachine and Test_meterRecords_FullLifecycleEmitsExactlyTwoRecords

@patrickhuie19 patrickhuie19 changed the title adding resource manager for wf size adding resource manager for workflows Jun 15, 2026
@patrickhuie19 patrickhuie19 added build-publish Build and Publish image to SDLC do not merge labels Jun 15, 2026
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

CORA - Pending Reviewers

Codeowners Entry Overall Num Files Owners
* 💬 33 @smartcontractkit/foundations, @smartcontractkit/core
/core/services/job/ 💬 1 @smartcontractkit/foundations, @smartcontractkit/core
/core/services/workflows/ 💬 12 @smartcontractkit/keystone
/core/services/standardcapabilities/ 💬 2 @smartcontractkit/keystone
/core/web/resolver/ 💬 3 @smartcontractkit/foundations, @smartcontractkit/core
go.mod 💬 6 @smartcontractkit/core, @smartcontractkit/foundations
go.sum 💬 6 @smartcontractkit/core, @smartcontractkit/foundations
integration-tests/go.mod 💬 1 @smartcontractkit/core, @smartcontractkit/devex-tooling, @smartcontractkit/foundations
integration-tests/go.sum 💬 1 @smartcontractkit/core, @smartcontractkit/devex-tooling, @smartcontractkit/foundations
/docs/CONFIG.md 💬 1 @smartcontractkit/foundations, @smartcontractkit/core, @smartcontractkit/devrel

Legend: ✅ Approved | ❌ Changes Requested | 💬 Commented | 🚫 Dismissed | ⏳ Pending | ❓ Unknown

For more details, see the full review summary.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

I see you updated files related to core. Please run make gocs in the root directory to add a changeset as well as in the text include at least one of the following tags:

  • #added For any new functionality added.
  • #breaking_change For any functionality that requires manual action for the node to boot.
  • #bugfix For bug fixes.
  • #changed For any change to the existing functionality.
  • #db_update For any feature that introduces updates to database schema.
  • #deprecation_notice For any upcoming deprecation functionality.
  • #internal For changesets that need to be excluded from the final changelog.
  • #nops For any feature that is NOP facing and needs to be in the official Release Notes for the release.
  • #removed For any functionality/config that is removed.
  • #updated For any functionality that is updated.
  • #wip For any change that is not ready yet and external communication about it should be held off till it is feature complete.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

✅ No conflicts with other open PRs targeting develop

@trunk-io

trunk-io Bot commented Jul 7, 2026

Copy link
Copy Markdown

Static BadgeStatic BadgeStatic Badge

Failed Test Failure Summary Logs
Test_CRE_V2_EVM_WriteReport_Invalid_Gas_Regression The test failed without providing specific error details. Logs ↗︎
Test_CRE_V2_EVM_WriteReport_Invalid_Gas_Regression/EVM.WriteReport_-_invalid_gas_fails_with_low The test failed because it did not find the expected user log message indicating invalid gas input. Logs ↗︎
Test_CRE_V2_EVM_WriteReport_Invalid_Gas_Regression/EVM.WriteReport_-_invalid_gas_fails_with_too_high The test failed because an invalid gas value caused the transaction to fail. Logs ↗︎
Test_CRE_V2_EVM_EstimateGas_Invalid_To_Address_Regression The test failed without providing specific error details, indicating an unspecified failure in the test case. Logs ↗︎

... and 107 more

View Full Report ↗︎Docs

…re-anchor

Phase 4 of the delta-based metering redesign, built against the new
chainlink-common resourcemanager API (EmitDelta/EmitUsage, RM-generated
event_id, no UtilizationFields.EventID).

TOML-only gating (kills split-brain):
- Delete env.MeterRecordsEnabled and the CL_METER_RECORDS_ENABLED LOOP
  pass-through (provably shadowed by AsCmdEnv) + its subtests.
- Delete cre.meterRecordsEnabled(); gate the syncer ResourceManager on
  cfg.Metering().MeterRecordsEnabled()/MeterSnapshotsEnabled().
- Default [Metering].Product = "cre" (accessor + core.toml + CONFIG.md).

Dead-plumbing removal:
- Remove standardcapabilities.NodeIdentity struct, delegate field/ctor param,
  and its population in application.go. Identity delivery is loop.EnvConfig.

node_id logical name:
- loop_registry already sources NodeID from [Metering].NodeID; fix fixtures
  (csa-pubkey-1 -> clp-cre-wf-zone-a-1) and drop the CSA-pubkey comment.

Host-side CapDONID (rule 8):
- Delegate already resolves and populates CapabilityDonID on the dependencies
  handed to capability LOOPs at Initialise; add a test asserting a nonzero
  CapDONID round-trips (and zero is preserved for the workflow-DON fallback).

Syncer v2 metering re-anchor (handler.go):
- Emit anchors on artifact persistence: EmitDelta(+1) when a new spec's
  artifacts are first persisted, EmitDelta(-1) when a real delete removes them.
  Pause/activate and status-only updates emit nothing.
- Delete emitGracefulCloseReleases and its call in close (no lifecycle emits).
- GetUtilization enumerates persisted specs (WorkflowSpecsDS.GetWorkflowSpecList)
  at value 1 per workflow; org resolved from the stored owner via the shared
  CachingOrgResolver. Records resolve org from payload owner via ResolveOrEmpty.
- Default ResourceManager to nil (nil-guarded), reuse one emitSpecDelta helper.

Regenerate txtar validate/merge goldens and effective TOMLs for the new
Product default and the Tenant/NumericTenantID dimensions.

Note: local replace directives point chainlink, core/scripts and deployment at
the ../chainlink-common and ../chainlink-protos/metering/go siblings so this
branch builds against the finished upstream API; the maintainer drops these and
bumps the module pins at merge time.
…w syncer

The common EmitDelta now requires a producer-supplied event_id. The syncer is a
workflow-DON service driven by reconciliation against the shared on-chain
WorkflowRegistry, so every workflow-DON node sees the same event and derives the
identical id via resourcemanager.EventID:

- create (+1): EventID("workflow-spec-register", workflowID, CreatedAt) — the
  on-chain CreatedAt is DON-consistent and distinguishes a re-registration from
  the original (re-activate-after-delete does not collide).
- delete (-1): EventID("workflow-spec-delete", workflowID). WorkflowDeletedEvent
  carries only the workflow ID (no on-chain CreatedAt/block), so repeated
  delete cycles of the same workflowID share an event_id; this residual drift is
  bounded by snapshot reconciliation. See the returned blocker note.

Scrubs the "UUIDv4" event_id wording and the UUID-format validation from the
metering test (event_id is an opaque string; the only check is empty-vs-nonempty).
Aligns the metering pin to the merged chainlink-common pin.
…w_specs_v2 to eliminate delta drift on register->pause->activate flow within billing dedup time bucket
@patrickhuie19
patrickhuie19 marked this pull request as ready for review July 31, 2026 14:37
@patrickhuie19
patrickhuie19 requested review from a team as code owners July 31, 2026 14:37
@cl-sonarqube-production

Copy link
Copy Markdown

Comment on lines +47 to 49
)

require (

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.

Stray indirect block

Suggested change
)
require (

Comment thread core/config/toml/types.go
// Product is the deployment product identity dimension, e.g. "cre".
Product *string
// Tenant is the human-readable tenant name, e.g. "mainline".
Tenant *string

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 different from the cresettings TenantID?

Comment thread core/config/toml/types.go
Comment on lines 67 to +68
Telemetry Telemetry `toml:",omitempty"`
Metering Metering `toml:",omitempty"`

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.

I wonder if just Metering is clear, especially alongside Telemetry. What about something like ResourceMetering? Or would it make sense as CRE.Metering?

}
h.emitSpecDelta(ctx, -1, workflowID, owner, resourcemanager.EventID("workflow-spec-delete", parts...))
}
h.cleanupModuleCache(workflowID)

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.

// does this belong here or with the caller?

}
var orgID string
if h.orgResolver != nil && spec.WorkflowOwner != "" {
if resolved, err := h.orgResolver.Get(ctx, spec.WorkflowOwner); err == nil {

@justinkaseman justinkaseman Jul 31, 2026

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.

Pointing out a discrepancy between what this says and how this is being implemented.

// GetUtilization returns the current level of the producer's currently
// active resources, one SnapshotEntry per resource. The manager emits one
// MeterSnapshot per entry.
//
// It is called on the snapshot tick and MUST be a cheap, non-blocking
// read-snapshot of in-memory state: no network, no disk, no lock held
// across I/O. It must tolerate ctx cancellation (returning promptly, and
// nil/empty is acceptable) and tolerate concurrent registration of new
// resources. An empty or nil return is valid and means nothing is currently
// active: no snapshots are emitted, and billing zeroes the resource out by
// its absence from subsequent snapshots.
GetUtilization(ctx context.Context) []SnapshotEntry

This is a gRPC round-trip plus JWT signing that is blocking.
The ResourceManager gives each registrant max(SnapshotInterval/4, 5s) = 15s at the 60s default. At ~100 workflows and ~150ms per lookup, the deadline already falls into fast fail mid-list.
The comment from chainlink-common would imply that there should be a cache here.

Caveat a cache will still miss the first run on a cold start

var orgID string
if h.orgResolver != nil && spec.WorkflowOwner != "" {
if resolved, err := h.orgResolver.Get(ctx, spec.WorkflowOwner); err == nil {
orgID = resolved

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.

Should we log a fail here? Otherwise an empty Org will be silently failing

}
var orgID string
if h.orgResolver != nil && owner != "" {
if resolved, err := h.orgResolver.Get(ctx, owner); err != nil {

@justinkaseman justinkaseman Jul 31, 2026

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.

Can we get this from context instead of refetching?

Might be a few places we could do this too

// through setDefaults returns "unset" (the nil-pointer fallback below).
func (b *meteringConfig) Product() string {
if b.s.Product == nil {
return "unset"

@justinkaseman justinkaseman Jul 31, 2026

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.

chainlink-common/pkg/resourcemanager.UnsetProduct ?

)
}

if !sourceFetchFailed && len(w.workflowSources) > 0 {

@justinkaseman justinkaseman Jul 31, 2026

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.

Might be worth adding some defense here against a source that returns an empty list (which I'm imagining could happen from an RPC having problems). Because if it hits that scenario it will clear all specs and -1 them, even though they might not be orphaned.

Guard could be if allMetadataIDs is empty while specs exist. We expect it won't be empty unless malfunctioning.

if rowsAffected == 0 {
return sql.ErrNoRows // No spec deleted
func (orm *orm) DeleteWorkflowSpec(ctx context.Context, id string) (*job.WorkflowSpec, error) {
query := `DELETE FROM workflow_specs_v2 WHERE workflow_id = $1 RETURNING *`

@justinkaseman justinkaseman Jul 31, 2026

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.

RETURNING * would be better as explicit columns like the other ORM methods

}

// SetWorkflowDon supplies the launcher-resolved workflow DON identity for
// metering. Called by the registry after WaitForDon, before any event is

@justinkaseman justinkaseman Aug 1, 2026

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.

Can the snapshot run before this is set? I see RM is .start'ed on event handler start

}

// syncUsingReconciliationStrategy syncs workflow registry contract state by polling the workflow metadata state and comparing to local state.
// NOTE: In this mode paused states will be treated as a deleted workflow. Workflows will not be registered as paused.

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.

Stale comment now with tombstones

@@ -875,6 +890,7 @@
if w.shardingEnabled {
filteredWorkflowsMetadata, err = w.filterWorkflowsByShard(ctx, workflows)

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.

Haven't had a chance to think through this yet, but note to self to double check everything works as expected with sharding moving a workflow off

// Test_specStorage_StateMachine exercises the storage state machine across
// (row state × event) transitions, verifying row existence, Status, artifact
// presence, RegisteredAt, engine-registry contents, and fetch call counts.
func Test_specStorage_StateMachine(t *testing.T) {

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.

Can we also test the new ORM methods against real pgtest like in orm_test.go?

copy(cp, h.released)
return cp
}

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.

The orphan sweeping could use more testing

  • A source succeeds with an empty list
  • One source fails, another succeeds. Nothing verifies it short-circuits the sweep.
  • Shard filter error sets the same flag at workflow_registry.go line 893
  • Sharding enabled, workflow filtered to another shard. Currently swept with a -1

@@ -0,0 +1,5 @@
-- +goose Up
ALTER TABLE workflow_specs_v2 ADD COLUMN registered_at bigint NOT NULL DEFAULT 0;

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 going to cause a problem if not all nodes update at the same time, and then with the mismatch a workflow gets deleted?

// event and derives the identical id from it (see the call sites). It is never
// generated here; an empty eventID is rejected by the ResourceManager.
func (h *eventHandler) emitSpecDelta(ctx context.Context, delta int64, workflowID, owner, eventID string) {
if h.resourceManager == nil {

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.

h.resourceManager is always non-nil because of https://github.com/smartcontractkit/chainlink/pull/22845/changes#diff-45d214eb19532a6195e33d2e90ee1b2b5534cea9c6ae823135c033ca5e2359b7R1060 even with metering flags off. So this guard doesn't do much

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

Labels

build-publish Build and Publish image to SDLC do not merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants