Files
temporal/chasm/registrable_component.go
Alan Wu 026450dca4 Add system search attribute registration option for CHASM library (#11080)
## What changed?
Support setting CHASM system search attribute overrides. Requires an
additional field in CHASM search attribute mapper. On write, loop
through any system search attributes and overwrite the visibility
request base. An additional switch case must be added per system search
attribute to support, since these values overwrite the visibility
request base.

## Why?
Allows users to reuse existing system columns for their CHASM search
attributes.

## 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)
2026-07-20 15:06:05 +00:00

234 lines
8.1 KiB
Go

package chasm
import (
"fmt"
"maps"
"reflect"
"github.com/dgryski/go-farm"
enumspb "go.temporal.io/api/enums/v1"
"go.temporal.io/server/common/searchattribute/sadefs"
)
type (
RegistrableComponent struct {
componentType string
goType reflect.Type
// Following three fields are initialized when the component is registered to a library.
library namer
componentID uint32
fqn string
ephemeral bool
singleCluster bool
detached bool
searchAttributesMapper *VisibilitySearchAttributesMapper
contextValues map[any]any
}
RegistrableComponentOption func(*RegistrableComponent)
)
func NewRegistrableComponent[C Component](
componentType string,
opts ...RegistrableComponentOption,
) *RegistrableComponent {
rc := &RegistrableComponent{
componentType: componentType,
goType: reflect.TypeFor[C](),
}
for _, opt := range opts {
opt(rc)
}
return rc
}
func WithEphemeral() RegistrableComponentOption {
return func(rc *RegistrableComponent) {
rc.ephemeral = true
}
}
// Is there any use case where we don't want to replicate certain instances of a archetype?
func WithSingleCluster() RegistrableComponentOption {
return func(rc *RegistrableComponent) {
rc.singleCluster = true
}
}
// WithDetached marks the registrable component as detached. Detached components ignore
// parent lifecycle validation, allowing them to continue operating when their
// parent is closed/terminated.
// If a registrable component is not detached by default, a component definition
// can specify its child as detached via ComponentFieldDetached() option.
func WithDetached() RegistrableComponentOption {
return func(rc *RegistrableComponent) {
rc.detached = true
}
}
// IsDetached returns true if the component type is registered as detached.
func (rc *RegistrableComponent) IsDetached() bool {
return rc.detached
}
// WithBusinessIDAlias allows specifying the business ID alias of the component.
// This option must be specified if the archetype uses the Visibility component.
func WithBusinessIDAlias(
alias string,
) RegistrableComponentOption {
return func(rc *RegistrableComponent) {
if rc.searchAttributesMapper == nil {
rc.searchAttributesMapper = newVisibilitySearchAttributesMapper()
}
if _, ok := rc.searchAttributesMapper.aliasToField[alias]; ok {
//nolint:forbidigo
panic(fmt.Sprintf("registrable component validation error: business ID alias %q is already defined as a search attribute", alias))
}
if _, ok := rc.searchAttributesMapper.systemAliasToField[alias]; ok {
//nolint:forbidigo
panic(fmt.Sprintf("registrable component validation error: business ID alias %q is already defined as a system search attribute", alias))
}
rc.searchAttributesMapper.systemAliasToField[alias] = sadefs.WorkflowID
rc.searchAttributesMapper.fieldToAlias[sadefs.WorkflowID] = alias
rc.searchAttributesMapper.saTypeMap[sadefs.WorkflowID] = enumspb.INDEXED_VALUE_TYPE_KEYWORD
}
}
func WithSearchAttributes(
searchAttributes ...SearchAttribute,
) RegistrableComponentOption {
return func(rc *RegistrableComponent) {
if len(searchAttributes) == 0 {
return
}
if rc.searchAttributesMapper == nil {
rc.searchAttributesMapper = newVisibilitySearchAttributesMapper()
}
for _, sa := range searchAttributes {
alias := sa.definition().alias
field := sa.definition().field
valueType := sa.definition().valueType
// An identity-mapped system search attribute (alias == field, e.g. TaskQueue,
// ExecutionTime) overrides that system column directly, so it is recorded only in
// overriddenSystemFields; queries resolve via the system column.
if field == alias && sadefs.IsSystem(field) {
if !sadefs.IsChasmOverridableSystem(field) {
//nolint:forbidigo
panic(fmt.Sprintf("registrable component validation error: system search attribute %q cannot be overridden by a CHASM component", field))
}
if _, ok := rc.searchAttributesMapper.overriddenSystemFields[field]; ok {
//nolint:forbidigo
panic(fmt.Sprintf("registrable component validation error: system search attribute override %q is already defined", field))
}
rc.searchAttributesMapper.overriddenSystemFields[field] = valueType
continue
}
if sadefs.IsChasmSystem(alias) {
//nolint:forbidigo
panic(fmt.Sprintf("registrable component validation error: CHASM search attribute alias %q is a CHASM system search attribute", alias))
}
if !sadefs.IsSystem(alias) && sadefs.IsReserved(alias) {
//nolint:forbidigo
panic(fmt.Sprintf("registrable component validation error: CHASM search attribute alias %q is a reserved search attribute", alias))
}
if _, ok := rc.searchAttributesMapper.systemAliasToField[alias]; ok {
//nolint:forbidigo
panic(fmt.Sprintf("registrable component validation error: CHASM search attribute alias %q is already defined as a system search attribute alias", alias))
}
if _, ok := rc.searchAttributesMapper.aliasToField[alias]; ok {
//nolint:forbidigo
panic(fmt.Sprintf("registrable component validation error: search attribute alias %q is already defined", alias))
}
if _, ok := rc.searchAttributesMapper.fieldToAlias[field]; ok {
//nolint:forbidigo
panic(fmt.Sprintf("registrable component validation error: search attribute field %q is already defined", field))
}
rc.searchAttributesMapper.aliasToField[alias] = field
rc.searchAttributesMapper.fieldToAlias[field] = alias
rc.searchAttributesMapper.saTypeMap[field] = valueType
}
}
}
// WithContextValues allows specifying key-value pairs that will be available in the Context
// via the Value() method whenever the chasm framework starts, updates, reads, polls, executes or
// validates tasks on a component.
//
// This is useful for propagating values needed for those processing logic but are not avaiable via the
// component's struct definition, such as configurations.
//
// Keys need to be globally unique across components. Conflicting keys across will cause component registration to fail.
//
// Manually added key-value pairs via ContextWithValue() will take precedence over registered context values.
func WithContextValues(
keyVals map[any]any,
) RegistrableComponentOption {
return func(rc *RegistrableComponent) {
if rc.contextValues == nil {
rc.contextValues = make(map[any]any, len(keyVals))
}
maps.Copy(rc.contextValues, keyVals)
}
}
func (rc *RegistrableComponent) registerToLibrary(
library namer,
) (string, uint32, error) {
if rc.library != nil {
return "", 0, fmt.Errorf("component %s is already registered in library %s", rc.componentType, rc.library.Name())
}
rc.library = library
rc.fqn = FullyQualifiedName(rc.library.Name(), rc.componentType)
rc.componentID = GenerateTypeID(rc.fqn)
return rc.fqn, rc.componentID, nil
}
// SearchAttributesMapper returns the search attributes mapper for this component.
func (rc *RegistrableComponent) SearchAttributesMapper() *VisibilitySearchAttributesMapper {
return rc.searchAttributesMapper
}
// GenerateTypeID generates a unique 32-bit identifier from a fully qualified name (FQN).
// The generated ID is used to uniquely identify components and tasks within the CHASM framework. The same FQN will
// always produce the same ID.
func GenerateTypeID(fqn string) uint32 {
return farm.Fingerprint32([]byte(fqn))
}
// hasBusinessIDAlias returns true if the component has a businessID alias configured
// via WithBusinessIDAlias option.
func (rc *RegistrableComponent) hasBusinessIDAlias() bool {
if rc.searchAttributesMapper == nil {
return false
}
_, ok := rc.searchAttributesMapper.fieldToAlias[sadefs.WorkflowID]
return ok
}
// GoType returns the reflect.Type of the component's Go struct.
func (rc *RegistrableComponent) GoType() reflect.Type {
return rc.goType
}
// fqType returns the fully qualified name of the component, which is a combination of
// the library name and the component type. This is used to uniquely identify
// the component in the registry.
func (rc *RegistrableComponent) fqType() string {
if rc.fqn == "" {
// this should never happen because the component is only accessible from the library.
panic("component is not registered to a library")
}
return rc.fqn
}