Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
6 changes: 6 additions & 0 deletions .changeset/four-panthers-itch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@apollo/composition": patch
"@apollo/federation-internals": patch
---

Adding new CompositionOption `maxValidationSubgraphPaths`. This value represents the maximum number of SubgraphPathInfo objects that may exist in a ValidationTraversal when checking for satisfiability. Setting this value can help composition error before running out of memory. Default is 1,000,000.
77 changes: 77 additions & 0 deletions composition-js/src/__tests__/validation_errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,3 +412,80 @@ describe('when shared field has non-intersecting runtime types in different subg
]);
});
});

describe('other validation errors', () => {

it('errors when maxValidationSubgraphPaths is exceeded', () => {
const subgraphA = {
name: 'A',
typeDefs: gql`
type Query {
a: A
}

type A @key(fields: "id") {
id: ID!
b: B
c: C
d: D
}

type B @key(fields: "id") {
id: ID!
a: A @shareable
b: Int @shareable
c: C @shareable
d: D @shareable
}

type C @key(fields: "id") {
id: ID!
a: A @shareable
b: B @shareable
c: Int @shareable
d: D @shareable
}

type D @key(fields: "id") {
id: ID!
a: A @shareable
b: B @shareable
c: C @shareable
d: Int @shareable
}
`
};
const subgraphB = {
name: 'B',
typeDefs: gql`
type B @key(fields: "id") {
id: ID!
b: Int @shareable
c: C @shareable
d: D @shareable
}

type C @key(fields: "id") {
id: ID!
b: B @shareable
c: Int @shareable
d: D @shareable
}

type D @key(fields: "id") {
id: ID!
b: B @shareable
c: C @shareable
d: Int @shareable
}
`
};
const result = composeAsFed2Subgraphs([subgraphA, subgraphB], { maxValidationSubgraphPaths: 10 });
expect(result.errors).toBeDefined();
expect(errorMessages(result)).toMatchStringArray([
`
Maximum number of validation subgraph paths exceeded: 12
`
]);
});
});
22 changes: 14 additions & 8 deletions composition-js/src/compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export interface CompositionOptions {
allowedFieldTypeMergingSubtypingRules?: SubtypingRule[];
/// Flag to toggle if satisfiability should be performed during composition
runSatisfiability?: boolean;
/// Maximum allowable number of outstanding subgraph paths to validate
maxValidationSubgraphPaths?: number;
}

function validateCompositionOptions(options: CompositionOptions) {
Expand All @@ -55,7 +57,7 @@ function validateCompositionOptions(options: CompositionOptions) {
* @param options CompositionOptions
*/
export function compose(subgraphs: Subgraphs, options: CompositionOptions = {}): CompositionResult {
const { runSatisfiability = true, sdlPrintOptions } = options;
const { runSatisfiability = true, sdlPrintOptions, maxValidationSubgraphPaths } = options;

validateCompositionOptions(options);

Expand All @@ -66,11 +68,15 @@ export function compose(subgraphs: Subgraphs, options: CompositionOptions = {}):

let satisfiabilityResult;
if (runSatisfiability) {
satisfiabilityResult = validateSatisfiability({
supergraphSchema: mergeResult.supergraph
});
if (satisfiabilityResult.errors) {
return { errors: satisfiabilityResult.errors };
try {
satisfiabilityResult = validateSatisfiability({
supergraphSchema: mergeResult.supergraph,
}, { maxValidationSubgraphPaths });
if (satisfiabilityResult.errors) {
return { errors: satisfiabilityResult.errors };
}
} catch (err) {
return { errors: [err] };
}
}

Expand Down Expand Up @@ -123,7 +129,7 @@ type SatisfiabilityArgs = {
* @param args: SatisfiabilityArgs
* @returns { errors? : GraphQLError[], hints? : CompositionHint[] }
*/
export function validateSatisfiability({ supergraphSchema, supergraphSdl} : SatisfiabilityArgs) : {
export function validateSatisfiability({ supergraphSchema, supergraphSdl} : SatisfiabilityArgs, options: CompositionOptions = {}) : {
errors? : GraphQLError[],
hints? : CompositionHint[],
} {
Expand All @@ -133,7 +139,7 @@ export function validateSatisfiability({ supergraphSchema, supergraphSdl} : Sati
const supergraph = supergraphSchema ? new Supergraph(supergraphSchema, null) : Supergraph.build(supergraphSdl, { supportedFeatures: null });
const supergraphQueryGraph = buildSupergraphAPIQueryGraph(supergraph);
const federatedQueryGraph = buildFederatedQueryGraph(supergraph, false);
return validateGraphComposition(supergraph.schema, supergraph.subgraphNameToGraphEnumValue(), supergraphQueryGraph, federatedQueryGraph);
return validateGraphComposition(supergraph.schema, supergraph.subgraphNameToGraphEnumValue(), supergraphQueryGraph, federatedQueryGraph, options);
}

type ValidateSubgraphsAndMergeResult = MergeResult | { errors: GraphQLError[] };
Expand Down
34 changes: 30 additions & 4 deletions composition-js/src/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import {
} from "@apollo/query-graphs";
import { CompositionHint, HINTS } from "./hints";
import { ASTNode, GraphQLError, print } from "graphql";
import { CompositionOptions } from './compose';

const debug = newDebugLogger('validation');

Expand Down Expand Up @@ -310,6 +311,7 @@ export function validateGraphComposition(
subgraphNameToGraphEnumValue: Map<string, string>,
supergraphAPI: QueryGraph,
federatedQueryGraph: QueryGraph,
compositionOptions: CompositionOptions = {},
): {
errors? : GraphQLError[],
hints? : CompositionHint[],
Expand All @@ -319,6 +321,7 @@ export function validateGraphComposition(
subgraphNameToGraphEnumValue,
supergraphAPI,
federatedQueryGraph,
compositionOptions,
).validate();
return errors.length > 0 ? { errors, hints } : { hints };
}
Expand Down Expand Up @@ -695,19 +698,26 @@ class ValidationTraversal {
private readonly validationHints: CompositionHint[] = [];

private readonly context: ValidationContext;

private totalValidationSubgraphPaths = 0;
private maxValidationSubgraphPaths: number;

private static DEFAULT_MAX_VALIDATION_SUBGRAPH_PATHS = 1000000;

constructor(
supergraphSchema: Schema,
subgraphNameToGraphEnumValue: Map<string, string>,
supergraphAPI: QueryGraph,
federatedQueryGraph: QueryGraph,
compositionOptions: CompositionOptions,
) {
this.maxValidationSubgraphPaths = compositionOptions.maxValidationSubgraphPaths ?? ValidationTraversal.DEFAULT_MAX_VALIDATION_SUBGRAPH_PATHS;

this.conditionResolver = simpleValidationConditionResolver({
supergraph: supergraphSchema,
queryGraph: federatedQueryGraph,
withCaching: true,
});
supergraphAPI.rootKinds().forEach((kind) => this.stack.push(ValidationState.initial({
supergraphAPI.rootKinds().forEach((kind) => this.pushStack(ValidationState.initial({
supergraphAPI,
kind,
federatedQueryGraph,
Expand All @@ -720,13 +730,29 @@ class ValidationTraversal {
subgraphNameToGraphEnumValue,
);
}

pushStack(state: ValidationState) {
this.totalValidationSubgraphPaths += state.subgraphPathInfos.length;
this.stack.push(state);
if (this.totalValidationSubgraphPaths > this.maxValidationSubgraphPaths) {
throw ERRORS.MAX_VALIDATION_SUBGRAPH_PATHS_EXCEEDED.err(`Maximum number of validation subgraph paths exceeded: ${this.totalValidationSubgraphPaths}`);
}
}

popStack() {
const state = this.stack.pop();
if (state) {
this.totalValidationSubgraphPaths -= state.subgraphPathInfos.length;
}
return state;
}

validate(): {
errors: GraphQLError[],
hints: CompositionHint[],
} {
while (this.stack.length > 0) {
this.handleState(this.stack.pop()!);
this.handleState(this.popStack()!);
}
return { errors: this.validationErrors, hints: this.validationHints };
}
Expand Down Expand Up @@ -799,7 +825,7 @@ class ValidationTraversal {
// state to the stack this method, `handleState`, will do nothing later. But it's
// worth checking it now and save some memory/cycles.
if (newState && !newState.supergraphPath.isTerminal()) {
this.stack.push(newState);
this.pushStack(newState);
debug.groupEnd(() => `Reached new state ${newState}`);
} else {
debug.groupEnd(`Reached terminal vertex/cycle`);
Expand Down
7 changes: 7 additions & 0 deletions internals-js/src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,12 @@ const LIST_SIZE_INVALID_SIZED_FIELD = makeCodeDefinition(
{ addedIn: '2.9.2' },
);

const MAX_VALIDATION_SUBGRAPH_PATHS_EXCEEDED = makeCodeDefinition(
'MAX_VALIDATION_SUBGRAPH_PATHS_EXCEEDED',
'The maximum number of validation subgraph paths has been exceeded.',
{ addedIn: '2.8.0' },
);

export const ERROR_CATEGORIES = {
DIRECTIVE_FIELDS_MISSING_EXTERNAL,
DIRECTIVE_UNSUPPORTED_ON_INTERFACE,
Expand Down Expand Up @@ -727,6 +733,7 @@ export const ERRORS = {
LIST_SIZE_INVALID_ASSUMED_SIZE,
LIST_SIZE_INVALID_SIZED_FIELD,
LIST_SIZE_INVALID_SLICING_ARGUMENT,
MAX_VALIDATION_SUBGRAPH_PATHS_EXCEEDED,
};

const codeDefByCode = Object.values(ERRORS).reduce((obj: {[code: string]: ErrorCodeDefinition}, codeDef: ErrorCodeDefinition) => { obj[codeDef.code] = codeDef; return obj; }, {});
Expand Down