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
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
`
]);
});
});
12 changes: 7 additions & 5 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 @@ -67,8 +69,8 @@ export function compose(subgraphs: Subgraphs, options: CompositionOptions = {}):
let satisfiabilityResult;
if (runSatisfiability) {
satisfiabilityResult = validateSatisfiability({
supergraphSchema: mergeResult.supergraph
});
supergraphSchema: mergeResult.supergraph,
}, { maxValidationSubgraphPaths });
if (satisfiabilityResult.errors) {
return { errors: satisfiabilityResult.errors };
}
Expand Down Expand Up @@ -123,7 +125,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 +135,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
46 changes: 40 additions & 6 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,18 +730,38 @@ class ValidationTraversal {
subgraphNameToGraphEnumValue,
);
}

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

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()!);
const { error } = this.handleState(this.popStack()!);
if (error) {
return { errors: [error], hints: this.validationHints };
}
}
return { errors: this.validationErrors, hints: this.validationHints };
}

private handleState(state: ValidationState) {
private handleState(state: ValidationState): { error?: GraphQLError } {
debug.group(() => `Validation: ${this.stack.length + 1} open states. Validating ${state}`);
const vertex = state.supergraphPath.tail;

Expand All @@ -748,7 +778,7 @@ class ValidationTraversal {
// type, and have strictly more options regarding subgraphs. So whatever comes next, we can handle in the exact
// same way we did previously, and there is thus no way to bother.
debug.groupEnd(`Has already validated this vertex.`);
return;
return {};
}
}
// We're gonna have to validate, but we can save the new set of sources here to hopefully save work later.
Expand Down Expand Up @@ -799,12 +829,16 @@ 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);
const { error } = this.pushStack(newState);
if (error) {
return { error };
}
debug.groupEnd(() => `Reached new state ${newState}`);
} else {
debug.groupEnd(`Reached terminal vertex/cycle`);
}
}
debug.groupEnd();
return {};
}
}
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