mirror of
https://github.com/temporalio/temporal.git
synced 2026-08-30 18:41:49 -07:00
## What changed? This PR aims to avoid usage of [`strings.Split`](https://pkg.go.dev/strings#Split) where possible in favor of better string splitting techniques, speficially: [`strings.SplitN`](https://pkg.go.dev/strings#SplitN) and [`strings.SplitSeq`](https://pkg.go.dev/strings#SplitSeq) where appropriate. There was also a [`strings.Fields`](https://pkg.go.dev/strings#Fields) change I made to use [`strings.FieldsSeq`](https://pkg.go.dev/strings#FieldsSeq) instead, and another for S3 to use the [`path`](https://pkg.go.dev/path) package instead of [`strings.Split`](https://pkg.go.dev/strings#Split). ## Why? [`strings.SplitN`](https://pkg.go.dev/strings#SplitN) and [`strings.SplitSeq`](https://pkg.go.dev/strings#SplitSeq) are often better options in many cases, and can be _partially_ detected using [`modernize`](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/modernize): > `stringsseq`: replace Split in "for range strings.Split(...)" by go1.24's more efficient `SplitSeq`, or `Fields` with `FieldSeq`. ## How did you test it? - [X] built - [X] run locally and tested manually - [x] covered by existing tests - [x] added new unit test(s) - [ ] added new functional test(s) ## Potential risks There are lots of potentially subtle behaviors from the `strings.Split` (and `strings.Fields`) usage that should be accounted for. If our existing tests don't cover those subtleties, there's risk for introducing an unintended bug. More intricate handling/parsing previously using the `strings` package should get extra attention from reviewers. I've attempted to break up my changes into logical commit chunks to aid in review / help spot potentially concerning changes.
172 lines
6.0 KiB
Go
172 lines
6.0 KiB
Go
package headers
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
|
|
"github.com/blang/semver/v4"
|
|
"go.temporal.io/api/serviceerror"
|
|
"google.golang.org/grpc/metadata"
|
|
)
|
|
|
|
const (
|
|
ClientNameServer = "temporal-server"
|
|
ClientNameServerHTTP = "temporal-server-http"
|
|
ClientNameGoSDK = "temporal-go"
|
|
ClientNameJavaSDK = "temporal-java"
|
|
ClientNamePHPSDK = "temporal-php"
|
|
ClientNameTypeScriptSDK = "temporal-typescript"
|
|
ClientNamePythonSDK = "temporal-python"
|
|
ClientNameCLI = "temporal-cli"
|
|
ClientNameUI = "temporal-ui"
|
|
ClientNameNexusGoSDK = "Nexus-go-sdk"
|
|
|
|
// ServerVersion value can be changed by the create-tag Github workflow.
|
|
// If you change the var name or move it, be sure to update the workflow.
|
|
ServerVersion = "1.29.0"
|
|
|
|
// SupportedServerVersions is used by CLI and inter role communication.
|
|
SupportedServerVersions = ">=1.0.0 <2.0.0"
|
|
|
|
// FeatureFollowsNextRunID means that the client supports following next execution run id for
|
|
// completed/failed/timedout completion events when getting the final result of a workflow.
|
|
FeatureFollowsNextRunID = "follows-next-run-id"
|
|
)
|
|
|
|
var (
|
|
// AllFeatures contains all known features. This list is used as the value of the supported
|
|
// features header for internal server requests. There is an assumption that if a feature is
|
|
// defined, then the server itself supports it.
|
|
AllFeatures = strings.Join([]string{
|
|
FeatureFollowsNextRunID,
|
|
}, SupportedFeaturesHeaderDelim)
|
|
|
|
SupportedClients = map[string]string{
|
|
ClientNameGoSDK: "<2.0.0",
|
|
ClientNameJavaSDK: "<2.0.0",
|
|
ClientNamePHPSDK: "<2.0.0",
|
|
ClientNameTypeScriptSDK: "<2.0.0",
|
|
ClientNameCLI: "<2.0.0",
|
|
ClientNameServer: "<2.0.0",
|
|
ClientNameUI: "<3.0.0",
|
|
ClientNameNexusGoSDK: "<2.0.0",
|
|
}
|
|
|
|
internalVersionHeaderPairs = []string{
|
|
ClientNameHeaderName, ClientNameServer,
|
|
ClientVersionHeaderName, ServerVersion,
|
|
SupportedServerVersionsHeaderName, SupportedServerVersions,
|
|
SupportedFeaturesHeaderName, AllFeatures,
|
|
}
|
|
)
|
|
|
|
type (
|
|
// VersionChecker is used to check client/server compatibility and client's capabilities
|
|
VersionChecker interface {
|
|
ClientSupported(ctx context.Context) error
|
|
ClientSupportsFeature(ctx context.Context, feature string) bool
|
|
}
|
|
|
|
versionChecker struct {
|
|
supportedClients map[string]string
|
|
supportedClientsRange map[string]semver.Range
|
|
serverVersion semver.Version
|
|
}
|
|
)
|
|
|
|
// NewDefaultVersionChecker constructs a new VersionChecker using default versions from const.
|
|
func NewDefaultVersionChecker() *versionChecker {
|
|
return NewVersionChecker(SupportedClients, ServerVersion)
|
|
}
|
|
|
|
// NewVersionChecker constructs a new VersionChecker
|
|
func NewVersionChecker(supportedClients map[string]string, serverVersion string) *versionChecker {
|
|
return &versionChecker{
|
|
serverVersion: semver.MustParse(serverVersion),
|
|
supportedClients: supportedClients,
|
|
supportedClientsRange: mustParseRanges(supportedClients),
|
|
}
|
|
}
|
|
|
|
// GetClientNameAndVersion extracts SDK name and version from context headers
|
|
func GetClientNameAndVersion(ctx context.Context) (string, string) {
|
|
headers := GetValues(ctx, ClientNameHeaderName, ClientVersionHeaderName)
|
|
clientName := headers[0]
|
|
clientVersion := headers[1]
|
|
return clientName, clientVersion
|
|
}
|
|
|
|
// SetVersions sets headers for internal communications.
|
|
func SetVersions(ctx context.Context) context.Context {
|
|
return metadata.AppendToOutgoingContext(ctx, internalVersionHeaderPairs...)
|
|
}
|
|
|
|
// SetVersionsForTests sets headers as they would be received from the client.
|
|
// Must be used in tests only.
|
|
func SetVersionsForTests(ctx context.Context, clientVersion, clientName, supportedServerVersions, supportedFeatures string) context.Context {
|
|
return metadata.NewIncomingContext(ctx, metadata.New(map[string]string{
|
|
ClientNameHeaderName: clientName,
|
|
ClientVersionHeaderName: clientVersion,
|
|
SupportedServerVersionsHeaderName: supportedServerVersions,
|
|
SupportedFeaturesHeaderName: supportedFeatures,
|
|
}))
|
|
}
|
|
|
|
// ClientSupported returns an error if client is unsupported, nil otherwise.
|
|
func (vc *versionChecker) ClientSupported(ctx context.Context) error {
|
|
|
|
headers := GetValues(ctx, ClientNameHeaderName, ClientVersionHeaderName, SupportedServerVersionsHeaderName)
|
|
clientName := headers[0]
|
|
clientVersion := headers[1]
|
|
supportedServerVersions := headers[2]
|
|
|
|
// Validate client version only if it is provided and server knows about this client.
|
|
if clientName != "" && clientVersion != "" {
|
|
if supportedClientRange, ok := vc.supportedClientsRange[clientName]; ok {
|
|
clientVersionParsed, parseErr := semver.Parse(clientVersion)
|
|
if parseErr != nil {
|
|
return serviceerror.NewInvalidArgumentf("Unable to parse client version: %v", parseErr)
|
|
}
|
|
if !supportedClientRange(clientVersionParsed) {
|
|
return serviceerror.NewClientVersionNotSupported(clientVersion, clientName, vc.supportedClients[clientName])
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate supported server version if it is provided.
|
|
if supportedServerVersions != "" {
|
|
supportedServerVersionsParsed, parseErr := semver.ParseRange(supportedServerVersions)
|
|
if parseErr != nil {
|
|
return serviceerror.NewInvalidArgumentf("Unable to parse supported server versions: %v", parseErr)
|
|
}
|
|
if !supportedServerVersionsParsed(vc.serverVersion) {
|
|
return serviceerror.NewServerVersionNotSupported(vc.serverVersion.String(), supportedServerVersions)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ClientSupportsFeature returns true if the client reports support for the
|
|
// given feature (which should be one of the Feature... constants above).
|
|
func (vc *versionChecker) ClientSupportsFeature(ctx context.Context, feature string) bool {
|
|
headers := GetValues(ctx, SupportedFeaturesHeaderName)
|
|
if len(headers) == 0 {
|
|
return false
|
|
}
|
|
for clientFeature := range strings.SplitSeq(headers[0], SupportedFeaturesHeaderDelim) {
|
|
if clientFeature == feature {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func mustParseRanges(ranges map[string]string) map[string]semver.Range {
|
|
out := make(map[string]semver.Range, len(ranges))
|
|
for c, r := range ranges {
|
|
out[c] = semver.MustParseRange(r)
|
|
}
|
|
return out
|
|
}
|