Skip to content

Commit 8a66571

Browse files
jiechenzclaude
andauthored
Allow handover and redirection interceptors to cover additional service prefixes (#10963)
## What changed? `NamespaceHandoverInterceptor` and `Redirection` short-circuit on the `WorkflowService` prefix, so requests to other frontend gRPC services skip handover gating and cross-cell redirection. Add two opt-in, default-no-op seams (WorkflowService behavior unchanged): - `NamespaceHandoverInterceptor.WithAdditionalServicePrefixes(...)` — gate additional service prefixes. - `Redirection.WithRedirectResponses(...)` — register methods as redirectable, keyed by full gRPC method; namespace resolved via the existing `NamespaceIDGetter`/`NamespaceNameGetter` path, fails closed. Neither references any embedding service; callers inject the prefix/response map. ## Why? Workflow-scoped requests on other frontend services must observe the same handover wait and active-cluster redirection as `WorkflowService`, or they can be served on a cell that no longer owns the partition during/after a handover. The prefix short-circuit made the interceptors no-ops for anything but `WorkflowService`. ## How did you test it? - [x] built - [x] covered by existing tests - [ ] run locally and tested manually - [ ] added new unit test(s) - [ ] added new functional test(s) Existing tests confirm the default (no-op) behavior is unchanged; the opt-in path is exercised by a downstream functional test. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b33bfbd commit 8a66571

2 files changed

Lines changed: 60 additions & 1 deletion

File tree

common/rpc/interceptor/namespace_handover.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ type (
3434
logger log.Logger
3535
requestErrorHandler ErrorHandler
3636
additionalAllowedMethodsDuringHandover map[string]struct{}
37+
// additionalServicePrefixes are gRPC service prefixes (besides WorkflowService) whose
38+
// methods the handover gate also applies to. Empty by default; embedders set these via
39+
// WithAdditionalServicePrefixes so the gate can cover other transports (e.g. a proxy
40+
// service) whose requests already expose their namespace, without this package knowing
41+
// about them.
42+
additionalServicePrefixes []string
3743
}
3844
)
3945

@@ -64,6 +70,29 @@ func NewNamespaceHandoverInterceptor(
6470
}
6571
}
6672

73+
// WithAdditionalServicePrefixes returns a copy of the interceptor whose handover gate also applies
74+
// to methods under the given gRPC service prefixes (besides WorkflowService). Embedders use this to
75+
// extend the gate to other transports without this package referencing them.
76+
func (i *NamespaceHandoverInterceptor) WithAdditionalServicePrefixes(prefixes ...string) *NamespaceHandoverInterceptor {
77+
clone := *i
78+
clone.additionalServicePrefixes = append(append([]string{}, i.additionalServicePrefixes...), prefixes...)
79+
return &clone
80+
}
81+
82+
// handlesMethod reports whether the handover gate applies to fullMethod: always for WorkflowService,
83+
// plus any embedder-configured service prefixes.
84+
func (i *NamespaceHandoverInterceptor) handlesMethod(fullMethod string) bool {
85+
if strings.HasPrefix(fullMethod, api.WorkflowServicePrefix) {
86+
return true
87+
}
88+
for _, prefix := range i.additionalServicePrefixes {
89+
if strings.HasPrefix(fullMethod, prefix) {
90+
return true
91+
}
92+
}
93+
return false
94+
}
95+
6796
func (i *NamespaceHandoverInterceptor) Intercept(
6897
ctx context.Context,
6998
req any,
@@ -72,7 +101,7 @@ func (i *NamespaceHandoverInterceptor) Intercept(
72101
) (_ any, retError error) {
73102
defer log.CapturePanic(i.logger, &retError)
74103

75-
if !strings.HasPrefix(info.FullMethod, api.WorkflowServicePrefix) {
104+
if !i.handlesMethod(info.FullMethod) {
76105
return handler(ctx, req)
77106
}
78107

common/rpc/interceptor/redirection.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,10 @@ type (
181181
clientBean client.Bean
182182
metricsHandler metrics.Handler
183183
timeSource clock.TimeSource
184+
// redirectResponsesByFullMethod registers embedder methods (keyed by full gRPC method, so
185+
// they don't collide with the bareredirectResponsesByFullMethod-name maps) as globally-redirectable, each mapped to its
186+
// response constructor. Nil by default, preserving the WorkflowService-only behavior.
187+
redirectResponsesByFullMethod map[string]responseConstructorFn
184188
}
185189
)
186190

@@ -215,6 +219,19 @@ func NewRedirection(
215219
}
216220
}
217221

222+
// WithRedirectResponses returns a copy of the interceptor that treats the given fullMethod ->
223+
// response-constructor entries as globally-redirectable APIs, keyed by full gRPC method so they
224+
// don't collide with the bare-method-name maps. The registered requests must expose their
225+
// namespace (via NamespaceNameGetter/NamespaceIDGetter) so it can be resolved for redirection.
226+
func (i *Redirection) WithRedirectResponses(responses map[string]func() any) *Redirection {
227+
clone := *i
228+
clone.redirectResponsesByFullMethod = make(map[string]responseConstructorFn, len(responses))
229+
for fullMethod, ctor := range responses {
230+
clone.redirectResponsesByFullMethod[fullMethod] = ctor
231+
}
232+
return &clone
233+
}
234+
218235
var _ grpc.UnaryServerInterceptor = (*Redirection)(nil).Intercept
219236

220237
func (i *Redirection) Intercept(
@@ -224,6 +241,19 @@ func (i *Redirection) Intercept(
224241
handler grpc.UnaryHandler,
225242
) (_ any, retError error) {
226243
defer log.CapturePanic(i.logger, &retError)
244+
if raFn, ok := i.redirectResponsesByFullMethod[info.FullMethod]; ok {
245+
if !i.RedirectionAllowed(ctx) {
246+
return handler(ctx, req)
247+
}
248+
// Resolve the namespace exactly like the WorkflowService global path below; the registered
249+
// request must expose it via NamespaceNameGetter/NamespaceIDGetter. Fails closed (returns
250+
// the error) rather than running a global-namespace request on a possibly non-owning cell.
251+
namespaceName, err := GetNamespaceName(i.namespaceCache, req)
252+
if err != nil {
253+
return nil, err
254+
}
255+
return i.handleRedirectAPIInvocation(ctx, req, info, handler, api.MethodName(info.FullMethod), raFn, namespaceName)
256+
}
227257

228258
if !strings.HasPrefix(info.FullMethod, api.WorkflowServicePrefix) {
229259
return handler(ctx, req)

0 commit comments

Comments
 (0)