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
15 changes: 14 additions & 1 deletion packages/indexer-agent/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,18 @@ module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier']
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
rules: {
// Pino emits a per-call field whose key matches a logger binding as a SECOND JSON
// key; strict parsers keep only the last. Forbid pino reserved keys as per-call fields.
'no-restricted-syntax': [
'error',
{
selector:
"CallExpression[callee.property.name=/^(trace|debug|info|warn|error|fatal)$/] > ObjectExpression > Property[key.name=/^(name|level|time|pid|hostname|msg|v)$/]",
message:
'Do not pass a pino reserved key (name, level, time, pid, hostname, msg, v) as a per-call log field: it collides with the logger bindings and emits a duplicate JSON key that strict parsers silently drop. Use a distinct key (e.g. subgraphName) or set bindings via logger.child({ ... }).',
},
],
},
}
83 changes: 80 additions & 3 deletions packages/indexer-agent/src/__tests__/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -524,14 +524,27 @@ describe('reconcileDeployments indexing-payments carve-out wiring', () => {
trace: jest.fn(),
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function createAgentUnderTest(network: any) {
function createAgentUnderTest(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
network: any,
dipsDeployments: SubgraphDeploymentID[] = [],
dipsManagerPresent = true,
) {
const operator = {
dipsManager: dipsManagerPresent
? {
getActiveDipsDeployments: jest
.fn()
.mockResolvedValue(dipsDeployments),
}
: null,
}
const agent = Object.create(Agent.prototype)
agent.logger = mockLogger
agent.offchainSubgraphs = []
agent.multiNetworks = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
map: async (fn: any) => Promise.all([fn({ network })]),
map: async (fn: any) => Promise.all([fn({ network, operator })]),
}
agent.graphNode = {
subgraphDeploymentsAssignments: jest.fn().mockResolvedValue([]),
Expand Down Expand Up @@ -581,4 +594,68 @@ describe('reconcileDeployments indexing-payments carve-out wiring', () => {

expect(agent.graphNode.ensure).not.toHaveBeenCalled()
})

it('does not pause an active deployment that has an active DIPS agreement', async () => {
const dipsDeployment = new SubgraphDeploymentID(
'QmWTbiUJQPEYDdUxt7sS8EWGwkxWmWTApJPaeGa4NXMqHQ',
)
const agent = createAgentUnderTest(
{
networkSubgraph: { deployment: undefined },
specification: { indexerOptions: { enableDips: true } },
indexingPaymentsSubgraph: { deployment: undefined },
},
[dipsDeployment],
)

// Active (being indexed) but absent from the target and eligible-allocation sets.
await agent.reconcileDeployments([dipsDeployment], [], [])

const pausedDeployments = agent.graphNode.pause.mock.calls.map(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(call: any[]) => (call[0] as SubgraphDeploymentID).bytes32,
)
expect(pausedDeployments).not.toContain(dipsDeployment.bytes32)
})

it('pauses an active deployment with no DIPS agreement that is not targeted', async () => {
const orphan = new SubgraphDeploymentID(
'QmNYBVzrWYrhmNF7srCs9qNUUnK1urXA1dNACrRp9xVPrH',
)
const agent = createAgentUnderTest(
{
networkSubgraph: { deployment: undefined },
specification: { indexerOptions: { enableDips: true } },
indexingPaymentsSubgraph: { deployment: undefined },
},
[],
)

await agent.reconcileDeployments([orphan], [], [])

const pausedDeployments = agent.graphNode.pause.mock.calls.map(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(call: any[]) => (call[0] as SubgraphDeploymentID).bytes32,
)
expect(pausedDeployments).toContain(orphan.bytes32)
})

it('skips DIPS protection without throwing when the DipsManager is not yet available', async () => {
const orphan = new SubgraphDeploymentID(
'QmNYBVzrWYrhmNF7srCs9qNUUnK1urXA1dNACrRp9xVPrH',
)
const agent = createAgentUnderTest(
{
networkSubgraph: { deployment: undefined },
specification: { indexerOptions: { enableDips: true } },
indexingPaymentsSubgraph: { deployment: undefined },
},
[],
false, // DIPS enabled, but the DipsManager has not been registered yet.
)

await expect(
agent.reconcileDeployments([orphan], [], []),
).resolves.not.toThrow()
})
})
89 changes: 68 additions & 21 deletions packages/indexer-agent/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,10 +296,13 @@ export class Agent {
async () => {
return this.multiNetworks.map(async ({ network, operator }) => {
if (network.specification.indexerOptions.enableDips) {
logger.debug('Ensuring indexing rules for DIPs', {
protocolNetwork: network.specification.networkIdentifier,
})
await operator.dipsManager!.ensureAgreementRules()
const dipsManager = this.readyDipsManager(network, operator)
if (dipsManager) {
logger.debug('Ensuring indexing rules for DIPs', {
protocolNetwork: network.specification.networkIdentifier,
})
await dipsManager.ensureAgreementRules()
}
} else {
logger.debug(
'DIPs is disabled, skipping indexing rule enforcement',
Expand Down Expand Up @@ -384,14 +387,14 @@ export class Agent {
protocolNetwork: network.specification.networkIdentifier,
})
const deployments = network.networkMonitor.subgraphDeployments()
if (network.specification.indexerOptions.enableDips) {
const dipsManager = network.specification.indexerOptions.enableDips
? this.readyDipsManager(network, operator)
: null
if (dipsManager) {
const resolvedDeployments = await deployments
const dipsDeployments = await Promise.all(
(await operator.dipsManager!.getActiveDipsDeployments()).map(
deployment =>
network.networkMonitor.subgraphDeployment(
deployment.ipfsHash,
),
(await dipsManager.getActiveDipsDeployments()).map(deployment =>
network.networkMonitor.subgraphDeployment(deployment.ipfsHash),
),
)
for (const deployment of dipsDeployments) {
Expand Down Expand Up @@ -580,7 +583,6 @@ export class Agent {
disputableAllocations,
}).pipe(
async ({
currentEpochNumber,
maxAllocationDuration,
activeDeployments,
targetDeployments,
Expand All @@ -589,8 +591,22 @@ export class Agent {
recentlyClosedAllocations,
disputableAllocations,
}) => {
// Read the epoch fresh once per pass instead of the separately-timed Eventual, which
// can lag the chain by an epoch and make a just-created allocation look stale.
const currentEpochNumberWithProvenance = await this.multiNetworks.map(
async ({ network }) =>
network.networkMonitor.currentEpochNumberWithProvenance(),
)
const currentEpochNumber = await this.multiNetworks.mapNetworkMapped(
currentEpochNumberWithProvenance,
async (
_: NetworkAndOperator,
provenance: { epoch: number; readAtBlock: number },
) => provenance.epoch,
)
logger.info(`Reconcile with the network`, {
currentEpochNumber,
epochProvenance: currentEpochNumberWithProvenance,
})

try {
Expand Down Expand Up @@ -649,22 +665,18 @@ export class Agent {
}
break
case DeploymentManagementMode.MANUAL:
await this.multiNetworks.map(async ({ network, operator }) => {
await this.multiNetworks.map(async ({ network }) => {
if (network.specification.indexerOptions.enableDips) {
// Reconcile DIPs deployments anyways
// Manual mode normally leaves deployments untouched, but reconcileDeployments
// keeps deployments with an active DIPS agreement out of the pause path, so
// still run it here; it resolves the active DIPS deployments itself.
this.logger.warn(
`Deployment management is manual, but DIPs is enabled. Reconciling DIPs deployments anyways.`,
)
const dipsDeployments =
await operator.dipsManager!.getActiveDipsDeployments()
const newTargetDeployments = new Set([
...activeDeployments,
...dipsDeployments,
])
try {
await this.reconcileDeployments(
activeDeployments,
Array.from(newTargetDeployments),
[...activeDeployments],
eligibleAllocations,
)
} catch (err) {
Expand Down Expand Up @@ -863,6 +875,22 @@ export class Agent {
})
}

// The DipsManager is built lazily once the action manager registers its allocation
// manager, so it can briefly be absent at startup even with DIPS enabled. Return it when
// ready, otherwise null with a debug log so callers skip their DIPS step rather than crash.
private readyDipsManager(
network: Network,
operator: Operator,
): Operator['dipsManager'] {
if (!operator.dipsManager) {
this.logger.debug(
'DIPS enabled but DipsManager not ready; skipping DIPS work this pass',
{ protocolNetwork: network.specification.networkIdentifier },
)
}
return operator.dipsManager
}

// This function assumes that allocations and deployments passed to it have already
// been retrieved from multiple networks.
async reconcileDeployments(
Expand Down Expand Up @@ -897,6 +925,25 @@ export class Agent {
)
})

// Keep deployments with an active DIPS agreement indexed even when a lagging or removed
// DIPS rule has dropped them from the target set; otherwise the pause path below would
// stop indexing a deployment the agreement is still paying for.
await this.multiNetworks.map(async ({ network, operator }) => {
if (!network.specification.indexerOptions.enableDips) {
return
}
const dipsManager = this.readyDipsManager(network, operator)
if (!dipsManager) {
return
}
const dipsDeployments = await dipsManager.getActiveDipsDeployments()
for (const deployment of dipsDeployments) {
if (!deploymentInList(targetDeployments, deployment)) {
targetDeployments.push(deployment)
}
}
})

// ----------------------------------------------------------------------------------------
// Inspect Deployments and Networks
// ----------------------------------------------------------------------------------------
Expand Down Expand Up @@ -954,7 +1001,7 @@ export class Agent {
const name = `indexer-agent/${deployment.ipfsHash.slice(-10)}`

logger.info(`Index subgraph deployment`, {
name,
subgraphName: name,
deployment: deployment.display,
})

Expand Down
2 changes: 1 addition & 1 deletion packages/indexer-agent/src/db/cli/umzug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const sequelize = new Sequelize({
logging: false,
})

logger.debug('Successfully connected to DB', { name: database })
logger.debug('Successfully connected to DB', { database })

export const migrator = new Umzug({
migrations: {
Expand Down
15 changes: 14 additions & 1 deletion packages/indexer-cli/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,18 @@ module.exports = {
root: false,
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier']
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
rules: {
// Pino emits a per-call field whose key matches a logger binding as a SECOND JSON
// key; strict parsers keep only the last. Forbid pino reserved keys as per-call fields.
'no-restricted-syntax': [
'error',
{
selector:
"CallExpression[callee.property.name=/^(trace|debug|info|warn|error|fatal)$/] > ObjectExpression > Property[key.name=/^(name|level|time|pid|hostname|msg|v)$/]",
message:
'Do not pass a pino reserved key (name, level, time, pid, hostname, msg, v) as a per-call log field: it collides with the logger bindings and emits a duplicate JSON key that strict parsers silently drop. Use a distinct key (e.g. subgraphName) or set bindings via logger.child({ ... }).',
},
],
},
}
15 changes: 14 additions & 1 deletion packages/indexer-common/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,18 @@ module.exports = {
root: false,
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier']
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
rules: {
// Pino emits a per-call field whose key matches a logger binding as a SECOND JSON
// key; strict parsers keep only the last. Forbid pino reserved keys as per-call fields.
'no-restricted-syntax': [
'error',
{
selector:
"CallExpression[callee.property.name=/^(trace|debug|info|warn|error|fatal)$/] > ObjectExpression > Property[key.name=/^(name|level|time|pid|hostname|msg|v)$/]",
message:
'Do not pass a pino reserved key (name, level, time, pid, hostname, msg, v) as a per-call log field: it collides with the logger bindings and emits a duplicate JSON key that strict parsers silently drop. Use a distinct key (e.g. subgraphName) or set bindings via logger.child({ ... }).',
},
],
},
}
Loading
Loading