Skip to content

Commit

Permalink
feat: add compatibility handshake between router and execution config (
Browse files Browse the repository at this point in the history
  • Loading branch information
Aenimus authored Jan 24, 2025
1 parent 1c7b0c9 commit 4b8d60a
Show file tree
Hide file tree
Showing 15 changed files with 1,585 additions and 1,337 deletions.
378 changes: 189 additions & 189 deletions composition-go/index.global.js

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion composition/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"test:watch": "vitest test",
"test": "vitest run",
"lint": "prettier --check src tests",
"lint:fix": "prettier --write src tests"
"lint:fix": "prettier --write src tests",
"postversion": "node ./scripts/get-composition-version.mjs"
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand Down
20 changes: 20 additions & 0 deletions composition/scripts/get-composition-version.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import fs from 'fs';
import { fileURLToPath } from 'node:url';
import path from 'node:path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compositionVersion = '{{$COMPOSITION__VERSION}}';

// From pnpm v10+, modules will explicitly need to set whether a hook is allowed to run.
if (process.argv[1] === __filename) {
const json = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json')).toString());
const version = json.version;
const varFilePath = path.join(__dirname, '../dist/utils/composition-version.js');
let content = fs.readFileSync(varFilePath).toString();
if (content.indexOf(compositionVersion) < 0) {
throw new Error(`"${compositionVersion}" string not found in dist/utils/composition-version.js.`);
}
content = content.replace(compositionVersion, version);
fs.writeFileSync(varFilePath, content);
}
3 changes: 2 additions & 1 deletion composition/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ export * from './schema-building/type-definition-data';
export * from './schema-building/type-merging';
export * from './schema-building/utils';
export * from './subgraph/subgraph';
export * from './utils/utils';
export * from './utils/composition-version';
export * from './utils/constants';
export * from './utils/utils';
export * from './utils/string-constants';
export * from './warnings/warnings';
1 change: 1 addition & 0 deletions composition/src/utils/composition-version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const COMPOSITION_VERSION = '{{$COMPOSITION__VERSION}}';
3 changes: 2 additions & 1 deletion composition/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"compilerOptions": {
"declaration": true,
"outDir": "./dist",
"module": "commonjs",
"module": "NodeNext",
"moduleResolution": "NodeNext",
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
Expand Down
1,151 changes: 581 additions & 570 deletions connect-go/gen/proto/wg/cosmo/node/v1/node.pb.go

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions connect/src/wg/cosmo/node/v1/node_pb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,11 @@ export class RouterConfig extends Message<RouterConfig> {
*/
featureFlagConfigs?: FeatureFlagRouterExecutionConfigs;

/**
* @generated from field: string compatibility_version = 5;
*/
compatibilityVersion = "";

constructor(data?: PartialMessage<RouterConfig>) {
super();
proto3.util.initPartial(data, this);
Expand All @@ -342,6 +347,7 @@ export class RouterConfig extends Message<RouterConfig> {
{ no: 2, name: "version", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 3, name: "subgraphs", kind: "message", T: Subgraph, repeated: true },
{ no: 4, name: "feature_flag_configs", kind: "message", T: FeatureFlagRouterExecutionConfigs, opt: true },
{ no: 5, name: "compatibility_version", kind: "scalar", T: 9 /* ScalarType.STRING */ },
]);

static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): RouterConfig {
Expand Down
1 change: 1 addition & 0 deletions proto/wg/cosmo/node/v1/node.proto
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ message RouterConfig {
repeated Subgraph subgraphs = 3;
// The map of feature flag router execution configs requires a wrapper to be non-breaking
optional FeatureFlagRouterExecutionConfigs feature_flag_configs = 4;
string compatibility_version = 5;
}

message Response {
Expand Down
10 changes: 10 additions & 0 deletions router/core/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -1105,6 +1105,16 @@ func (r *Router) Start(ctx context.Context) error {
return nil
}

/* Older versions of composition will not populate a compatibility version.
* Currently, all "old" router execution configurations are compatible as there have been no breaking
* changes.
* Upon the first breaking change to the execution config, an unpopulated compatibility version will
* also be unsupported (and the logic for IsRouterCompatibleWithExecutionConfig will need to be updated).
*/
if !execution_config.IsRouterCompatibleWithExecutionConfig(r.logger, cfg.CompatibilityVersion) {
return nil
}

if err := r.newServer(ctx, cfg); err != nil {
r.logger.Error("Failed to update server with new config", zap.Error(err))
return nil
Expand Down
1,151 changes: 581 additions & 570 deletions router/gen/proto/wg/cosmo/node/v1/node.pb.go

Large diffs are not rendered by default.

72 changes: 72 additions & 0 deletions router/pkg/execution_config/compatibility.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package execution_config

import (
"fmt"
"go.uber.org/zap"
"strconv"
"strings"
)

const (
// ExecutionConfigVersionThreshold should ONLY be updated if there is a breaking change in the router execution config.
ExecutionConfigVersionThreshold = 1
compatibilityVersionParseErrorMessage = "Failed to parse compatibility version."
executionConfigVersionParseErrorMessage = "Failed to parse router execution config version of compatibility version."
)

func IsRouterCompatibleWithExecutionConfig(logger *zap.Logger, compatibilityVersion string) bool {
if compatibilityVersion == "" {
return true
}
/* A compatibility version is composed thus: <router execution configuration version>:<composition package version>
* A router version supports a maximum router execution configuration version (ExecutionConfigVersionThreshold).
* In the event the execution config version exceeds ExecutionConfigVersionThreshold, an error will request for
* the router version be upgraded.
* If the router version requires a newer router execution configuration version, a warning will explain that some
* new features may be unavailable or functionality/behaviour may have changed.
*/
segments := strings.Split(compatibilityVersion, ":")
if len(segments) != 2 {
logger.Error(compatibilityVersionParseErrorMessage, zap.String("compatibility_version", compatibilityVersion))
return false
}
routerExecutionVersion, err := strconv.ParseInt(segments[0], 10, 32)
if err != nil {
logger.Error(executionConfigVersionParseErrorMessage, zap.String("compatibility_version", compatibilityVersion))
return false
}
switch {
case routerExecutionVersion == ExecutionConfigVersionThreshold:
return true
case routerExecutionVersion > ExecutionConfigVersionThreshold:
logger.Error(
executionConfigVersionThresholdExceededError(routerExecutionVersion),
zap.Int64("execution_config_version", routerExecutionVersion),
zap.String("composition_package_version", segments[1]),
)
return false
default:
logger.Warn(
executionConfigVersionInsufficientWarning(routerExecutionVersion),
zap.Int64("execution_config_version", routerExecutionVersion),
zap.String("composition_package_version", segments[1]),
)
return true
}
}

func executionConfigVersionThresholdExceededError(executionConfigVersion int64) string {
return fmt.Sprintf(
"This router version supports a router execution config version up to %d. The router execution config version supplied is %d. Please upgrade your router version.",
ExecutionConfigVersionThreshold,
executionConfigVersion,
)
}

func executionConfigVersionInsufficientWarning(executionConfigVersion int64) string {
return fmt.Sprintf(
"This router version requires a minimum router execution config version of %d to support all functionality. The router execution config version supplied is %d. Please create a new execution configuration.",
ExecutionConfigVersionThreshold,
executionConfigVersion,
)
}
108 changes: 108 additions & 0 deletions router/pkg/execution_config/compatibility_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package execution_config

import (
"bytes"
"fmt"
"github.com/stretchr/testify/assert"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"go.uber.org/zap/zaptest/observer"
"testing"
)

func TestExecutionConfiguration(t *testing.T) {
t.Run("no compatibility version is supported", func(t *testing.T) {
observed, logs := observer.New(zapcore.DebugLevel)
logger := newLogger(observed)
assert.True(t, IsRouterCompatibleWithExecutionConfig(logger, ""))
assert.Equal(t, 0, len(logs.All()))
})

t.Run("same compatibility version is supported", func(t *testing.T) {
observed, logs := observer.New(zapcore.DebugLevel)
logger := newLogger(observed)
assert.True(t, IsRouterCompatibleWithExecutionConfig(logger, fmt.Sprintf("%d:0.1.0", ExecutionConfigVersionThreshold)))
assert.Equal(t, 0, len(logs.All()))
})

t.Run("return an error if compatibility version is unparsable #1", func(t *testing.T) {
observed, logs := observer.New(zapcore.DebugLevel)
logger := newLogger(observed)
compatibilityVersion := "nonsense"
assert.False(t, IsRouterCompatibleWithExecutionConfig(logger, compatibilityVersion))
logsSlice := logs.All()
assert.Equal(t, 1, len(logsSlice))
assert.Equal(t, compatibilityVersionParseErrorMessage, logsSlice[0].Message)
assert.Equal(t, zapcore.ErrorLevel, logsSlice[0].Level)
assert.Equal(t, 1, len(logsSlice[0].Context))
assert.Equal(t, zap.String("compatibility_version", compatibilityVersion), logsSlice[0].Context[0])
})

t.Run("return an error if compatibility version is unparsable #2", func(t *testing.T) {
observed, logs := observer.New(zapcore.DebugLevel)
logger := newLogger(observed)
compatibilityVersion := "1:2:3"
assert.False(t, IsRouterCompatibleWithExecutionConfig(logger, compatibilityVersion))
logsSlice := logs.All()
assert.Equal(t, 1, len(logsSlice))
assert.Equal(t, compatibilityVersionParseErrorMessage, logsSlice[0].Message)
assert.Equal(t, zapcore.ErrorLevel, logsSlice[0].Level)
assert.Equal(t, 1, len(logsSlice[0].Context))
assert.Equal(t, zap.String("compatibility_version", compatibilityVersion), logsSlice[0].Context[0])
})

t.Run("return an error if execution config version is unparsable", func(t *testing.T) {
observed, logs := observer.New(zapcore.DebugLevel)
logger := newLogger(observed)
compatibilityVersion := "a:0.1.0"
assert.False(t, IsRouterCompatibleWithExecutionConfig(logger, compatibilityVersion))
logsSlice := logs.All()
assert.Equal(t, 1, len(logsSlice))
assert.Equal(t, executionConfigVersionParseErrorMessage, logsSlice[0].Message)
assert.Equal(t, zapcore.ErrorLevel, logsSlice[0].Level)
assert.Equal(t, 1, len(logsSlice[0].Context))
assert.Equal(t, zap.String("compatibility_version", compatibilityVersion), logsSlice[0].Context[0])
})

t.Run("return an error if the maximum execution config version threshold of the router is exceeded", func(t *testing.T) {
observed, logs := observer.New(zapcore.DebugLevel)
logger := newLogger(observed)
nextVersion := int64(ExecutionConfigVersionThreshold + 1)
compVersion := "0.1.0"
compatibilityVersion := fmt.Sprintf("%d:%s", nextVersion, compVersion)
assert.False(t, IsRouterCompatibleWithExecutionConfig(logger, compatibilityVersion))
logsSlice := logs.All()
assert.Equal(t, 1, len(logsSlice))
assert.Equal(t, executionConfigVersionThresholdExceededError(nextVersion), logsSlice[0].Message)
assert.Equal(t, zapcore.ErrorLevel, logsSlice[0].Level)
assert.Equal(t, 2, len(logsSlice[0].Context))
assert.Equal(t, zap.Int64("execution_config_version", nextVersion), logsSlice[0].Context[0])
assert.Equal(t, zap.String("composition_package_version", compVersion), logsSlice[0].Context[1])
})

t.Run("return a warning if the execution config version is insufficient", func(t *testing.T) {
observed, logs := observer.New(zapcore.DebugLevel)
logger := newLogger(observed)
previousVersion := int64(ExecutionConfigVersionThreshold - 1)
compVersion := "0.1.0"
compatibilityVersion := fmt.Sprintf("%d:%s", previousVersion, compVersion)
assert.True(t, IsRouterCompatibleWithExecutionConfig(logger, compatibilityVersion))
logsSlice := logs.All()
assert.Equal(t, 1, len(logsSlice))
assert.Equal(t, executionConfigVersionInsufficientWarning(previousVersion), logsSlice[0].Message)
assert.Equal(t, zapcore.WarnLevel, logsSlice[0].Level)
assert.Equal(t, 2, len(logsSlice[0].Context))
assert.Equal(t, zap.Int64("execution_config_version", previousVersion), logsSlice[0].Context[0])
assert.Equal(t, zap.String("composition_package_version", compVersion), logsSlice[0].Context[1])
})
}

func newLogger(observed zapcore.Core) *zap.Logger {
var buffer bytes.Buffer
return zap.New(
zapcore.NewTee(
zapcore.NewCore(zapcore.NewJSONEncoder(zapcore.EncoderConfig{}), zapcore.AddSync(&buffer), zapcore.DebugLevel),
observed,
),
)
}
3 changes: 2 additions & 1 deletion shared/src/router-config/builder.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import crypto from 'node:crypto';
import { printSchemaWithDirectives } from '@graphql-tools/utils';
import { ConfigurationData, FieldConfiguration, ROOT_TYPE_NAMES } from '@wundergraph/composition';
import { COMPOSITION_VERSION, ConfigurationData, FieldConfiguration, ROOT_TYPE_NAMES } from '@wundergraph/composition';
import { GraphQLSchema, lexicographicSortSchema } from 'graphql';
import {
GraphQLSubscriptionProtocol,
Expand Down Expand Up @@ -215,5 +215,6 @@ export const buildRouterConfig = function (input: Input): RouterConfig {
name: s.name,
routingUrl: s.url,
})),
compatibilityVersion: `1:${COMPOSITION_VERSION}`,
});
};
12 changes: 8 additions & 4 deletions shared/test/__snapshots__/router.config.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,8 @@ exports[`Router Config Builder > Build Subgraph schema > router.config.json 1`]
"name": "inventory",
"routingUrl": "https://wg-federation-demo-inventory.fly.dev/graphql"
}
]
],
"compatibilityVersion": "1:{{$COMPOSITION__VERSION}}"
}"
`;

Expand Down Expand Up @@ -436,7 +437,8 @@ exports[`Router Config Builder > that the federatedClientSDL property is not pro
"name": "products",
"routingUrl": "https://wg-federation-demo-products.fly.dev/graphql"
}
]
],
"compatibilityVersion": "1:{{$COMPOSITION__VERSION}}"
}"
`;

Expand Down Expand Up @@ -571,7 +573,8 @@ exports[`Router Config Builder > that the federatedClientSDL property is propaga
"name": "products",
"routingUrl": "https://wg-federation-demo-products.fly.dev/graphql"
}
]
],
"compatibilityVersion": "1:{{$COMPOSITION__VERSION}}"
}"
`;

Expand Down Expand Up @@ -706,6 +709,7 @@ exports[`Router Config Builder > that the federatedClientSDL property is propaga
"name": "products",
"routingUrl": "https://wg-federation-demo-products.fly.dev/graphql"
}
]
],
"compatibilityVersion": "1:{{$COMPOSITION__VERSION}}"
}"
`;

0 comments on commit 4b8d60a

Please sign in to comment.