Skip to content

Commit 24923a8

Browse files
committed
Experimental API example integration
1 parent 66083d0 commit 24923a8

18 files changed

Lines changed: 467 additions & 19 deletions

Makefile

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,14 @@ VISIBILITY_DB ?= temporal_visibility
4747
# The `disable_grpc_modules` build tag excludes gRPC dependencies from cloud.google.com/go/storage,
4848
# reducing binary size by 16MB since we only use the REST client (storage.NewClient), not the
4949
# gRPC client (storage.NewGRPCClient). Related issue: https://github.com/googleapis/google-cloud-go/issues/12343
50+
#
51+
# The `experimental` tag is added to ALL_TEST_TAGS (not ALL_BUILD_TAGS) so
52+
# the production binary is built without it but every test invocation gets
53+
# it by default. This keeps the test/dev path realistic (variants pickable
54+
# per cluster via dynconfig) while ensuring `make build` of temporal-server
55+
# never accidentally pulls in experimental code or its api-go modules.
5056
ALL_BUILD_TAGS := disable_grpc_modules,$(BUILD_TAG)
51-
ALL_TEST_TAGS := $(ALL_BUILD_TAGS),test_dep,$(TEST_TAG)
57+
ALL_TEST_TAGS := $(ALL_BUILD_TAGS),test_dep,experimental,$(TEST_TAG)
5258
BUILD_TAG_FLAG := -tags $(ALL_BUILD_TAGS)
5359
TEST_TAG_FLAG := -tags $(ALL_TEST_TAGS)
5460

common/dynamicconfig/constants.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,26 @@ values in system search attributes.`,
9999
query.`,
100100
)
101101

102+
// FrontendAPIVariant selects which frontend API variant the
103+
// frontend exposes at startup. Empty (default) registers stable
104+
// WorkflowService only. A non-empty value (e.g. "ping", "tinker") looks
105+
// up the named variant in the experimental registry and registers it
106+
// IN PLACE OF stable WorkflowService at the same wire path; the variant
107+
// delegates stable methods to the existing stable handler. Toggling
108+
// requires a frontend restart.
109+
//
110+
// A variant name is only resolvable if the corresponding build tag was
111+
// set (e.g. -tags experimental). With no tag, the registry is
112+
// empty and any non-empty value here causes Start() to log Fatal —
113+
// production binaries cannot accidentally expose experimental surface.
114+
FrontendAPIVariant = NewGlobalStringSetting(
115+
"frontend.apiVariant",
116+
"",
117+
`FrontendAPIVariant selects which frontend API variant the
118+
frontend exposes. Empty = stable only. See service/frontend/services/
119+
for the list of supported variants in this build.`,
120+
)
121+
102122
HistoryArchivalState = NewGlobalStringSetting(
103123
"system.historyArchivalState",
104124
"", // actual default is from static config

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ require (
4646
github.com/robfig/cron/v3 v3.0.1
4747
github.com/sony/gobreaker v1.0.0
4848
github.com/stretchr/testify v1.11.1
49+
github.com/temporalio/api-go/experimental/example v0.0.0-20260425220524-eb1c7feb8022
4950
github.com/temporalio/ringpop-go v0.0.0-20250130211428-b97329e994f7
5051
github.com/temporalio/sqlparser v0.0.0-20231115171017-f4060bcfa6cb
5152
github.com/temporalio/tchannel-go v1.22.1-0.20260129151045-8706a1ab5f61

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
394394
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
395395
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
396396
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
397+
github.com/temporalio/api-go/experimental/example v0.0.0-20260425220524-eb1c7feb8022 h1:C3guzElkL2xWFKhLoXw9TJn6OAh8XjbMona2cJo3rhs=
398+
github.com/temporalio/api-go/experimental/example v0.0.0-20260425220524-eb1c7feb8022/go.mod h1:51vB7ml3vLZ4cxGSW9JQVjbTdmWeZd4igRb2kq7kdt4=
397399
github.com/temporalio/ringpop-go v0.0.0-20250130211428-b97329e994f7 h1:lEebX/hZss+TSH3EBwhztnBavJVj7pWGJOH8UgKHS0w=
398400
github.com/temporalio/ringpop-go v0.0.0-20250130211428-b97329e994f7/go.mod h1:RE+CHmY+kOZQk47AQaVzwrGmxpflnLgTd6EOK0853j4=
399401
github.com/temporalio/sqlparser v0.0.0-20231115171017-f4060bcfa6cb h1:YzHH/U/dN7vMP+glybzcXRTczTrgfdRisNTzAj7La04=

service/frontend/service.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@ import (
66
"sync"
77
"time"
88

9-
"go.temporal.io/api/operatorservice/v1"
10-
"go.temporal.io/api/workflowservice/v1"
11-
"go.temporal.io/server/api/adminservice/v1"
129
"go.temporal.io/server/chasm/lib/activity"
1310
chasmnexus "go.temporal.io/server/chasm/lib/nexusoperation"
1411
"go.temporal.io/server/common/dynamicconfig"
@@ -20,6 +17,7 @@ import (
2017
"go.temporal.io/server/common/retrypolicy"
2118
"go.temporal.io/server/components/callbacks"
2219
"go.temporal.io/server/components/nexusoperations"
20+
"go.temporal.io/server/service/frontend/services"
2321
"google.golang.org/grpc"
2422
"google.golang.org/grpc/health"
2523
healthpb "google.golang.org/grpc/health/grpc_health_v1"
@@ -232,6 +230,10 @@ type Config struct {
232230
HTTPAllowedHosts dynamicconfig.TypedPropertyFn[*regexp.Regexp]
233231
AllowedExperiments dynamicconfig.TypedPropertyFnWithNamespaceFilter[[]string]
234232

233+
// APIVariant selects which frontend API variant the
234+
// frontend exposes at startup. See service/frontend/services/.
235+
APIVariant dynamicconfig.StringPropertyFn
236+
235237
// CHASM archetypes
236238
Activity *activity.Config
237239
}
@@ -400,6 +402,8 @@ func NewConfig(
400402
HTTPAllowedHosts: dynamicconfig.FrontendHTTPAllowedHosts.Get(dc),
401403
AllowedExperiments: dynamicconfig.FrontendAllowedExperiments.Get(dc),
402404

405+
APIVariant: dynamicconfig.FrontendAPIVariant.Get(dc),
406+
403407
Activity: activity.ConfigProvider(dc),
404408
}
405409
}
@@ -456,13 +460,19 @@ func NewService(
456460
}
457461

458462
// Start starts the service
463+
459464
func (s *Service) Start() {
460465
s.logger.Info("frontend starting")
461466

462467
healthpb.RegisterHealthServer(s.server, s.healthServer)
463-
workflowservice.RegisterWorkflowServiceServer(s.server, s.handler)
464-
adminservice.RegisterAdminServiceServer(s.server, s.adminHandler)
465-
operatorservice.RegisterOperatorServiceServer(s.server, s.operatorHandler)
468+
services.Register(services.Registration{
469+
Server: s.server,
470+
Workflow: s.handler,
471+
Admin: s.adminHandler,
472+
Operator: s.operatorHandler,
473+
Variant: s.config.APIVariant(),
474+
Logger: s.logger,
475+
})
466476

467477
reflection.Register(s.server)
468478

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//go:build experimental
2+
3+
package services
4+
5+
import (
6+
"context"
7+
8+
expexample "github.com/temporalio/api-go/experimental/example/workflowservice/v1"
9+
"go.temporal.io/api/workflowservice/v1"
10+
"google.golang.org/grpc"
11+
)
12+
13+
type exampleHandler struct{}
14+
15+
func (exampleHandler) Echo(_ context.Context, req *expexample.EchoRequest) (*expexample.EchoResponse, error) {
16+
return &expexample.EchoResponse{Payload: req.GetPayload()}, nil
17+
}
18+
19+
func init() {
20+
register("example", variant{registerWorkflow: func(server *grpc.Server, workflow workflowservice.WorkflowServiceServer) {
21+
registerServiceOverlay(server, workflowservice.WorkflowService_ServiceDesc, workflow, expexample.WorkflowService_ServiceDesc, exampleHandler{})
22+
}})
23+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package services
2+
3+
import (
4+
"go.temporal.io/api/operatorservice/v1"
5+
"go.temporal.io/api/workflowservice/v1"
6+
"go.temporal.io/server/api/adminservice/v1"
7+
"go.temporal.io/server/common/log"
8+
"go.temporal.io/server/common/log/tag"
9+
"google.golang.org/grpc"
10+
)
11+
12+
type variant struct {
13+
registerWorkflow func(*grpc.Server, workflowservice.WorkflowServiceServer)
14+
registerAdmin func(*grpc.Server, adminservice.AdminServiceServer)
15+
registerOperator func(*grpc.Server, operatorservice.OperatorServiceServer)
16+
}
17+
18+
type Registration struct {
19+
Server *grpc.Server
20+
Workflow workflowservice.WorkflowServiceServer
21+
Admin adminservice.AdminServiceServer
22+
Operator operatorservice.OperatorServiceServer
23+
Variant string
24+
Logger log.Logger
25+
}
26+
27+
// Register installs the stable frontend gRPC services, or an experimental
28+
// variant compiled into this binary.
29+
func Register(r Registration) {
30+
if err := registryError(); err != nil {
31+
r.Logger.Fatal("invalid experimental API registry", tag.Error(err))
32+
}
33+
34+
selected := variant{}
35+
if r.Variant == "" {
36+
registerVariant(r, selected)
37+
return
38+
}
39+
40+
var ok bool
41+
selected, ok = get(r.Variant)
42+
if !ok {
43+
r.Logger.Fatal(
44+
"frontend.apiVariant set but variant not wired into this binary",
45+
tag.NewStringTag("variant", r.Variant),
46+
tag.NewStringsTag("compiled_in", names()),
47+
)
48+
}
49+
50+
registerVariant(r, selected)
51+
r.Logger.Info("Experimental API variant active",
52+
tag.NewStringTag("variant", r.Variant))
53+
}
54+
55+
func registerVariant(r Registration, v variant) {
56+
registerWorkflow := registerStableWorkflow
57+
if v.registerWorkflow != nil {
58+
registerWorkflow = v.registerWorkflow
59+
}
60+
registerAdmin := registerStableAdmin
61+
if v.registerAdmin != nil {
62+
registerAdmin = v.registerAdmin
63+
}
64+
registerOperator := registerStableOperator
65+
if v.registerOperator != nil {
66+
registerOperator = v.registerOperator
67+
}
68+
69+
registerWorkflow(r.Server, r.Workflow)
70+
registerAdmin(r.Server, r.Admin)
71+
registerOperator(r.Server, r.Operator)
72+
}
73+
74+
func registerStableWorkflow(server *grpc.Server, workflow workflowservice.WorkflowServiceServer) {
75+
workflowservice.RegisterWorkflowServiceServer(server, workflow)
76+
}
77+
78+
func registerStableAdmin(server *grpc.Server, admin adminservice.AdminServiceServer) {
79+
adminservice.RegisterAdminServiceServer(server, admin)
80+
}
81+
82+
func registerStableOperator(server *grpc.Server, operator operatorservice.OperatorServiceServer) {
83+
operatorservice.RegisterOperatorServiceServer(server, operator)
84+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//go:build !experimental
2+
3+
package services
4+
5+
func get(string) (variant, bool) {
6+
return variant{}, false
7+
}
8+
9+
func names() []string {
10+
return nil
11+
}
12+
13+
func registryError() error {
14+
return nil
15+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//go:build experimental
2+
3+
package services
4+
5+
import "fmt"
6+
7+
var entries = map[string]variant{}
8+
var registryErr error
9+
10+
func register(name string, v variant) {
11+
if _, dup := entries[name]; dup {
12+
registryErr = errorsJoin(registryErr, fmt.Errorf("duplicate registration for variant %s", name))
13+
return
14+
}
15+
entries[name] = v
16+
}
17+
18+
func get(name string) (variant, bool) {
19+
v, ok := entries[name]
20+
return v, ok
21+
}
22+
23+
func names() []string {
24+
out := make([]string, 0, len(entries))
25+
for name := range entries {
26+
out = append(out, name)
27+
}
28+
return out
29+
}
30+
31+
func registryError() error {
32+
return registryErr
33+
}
34+
35+
func errorsJoin(current error, next error) error {
36+
if current == nil {
37+
return next
38+
}
39+
return fmt.Errorf("%w; %w", current, next)
40+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//go:build experimental
2+
3+
package services
4+
5+
import (
6+
"sort"
7+
"testing"
8+
)
9+
10+
func TestRegisteredVariants(t *testing.T) {
11+
names := names()
12+
sort.Strings(names)
13+
t.Logf("variants compiled in: %v", names)
14+
}

0 commit comments

Comments
 (0)