Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
6f6fdf3
Added the notification channel library and its history service.
moetemp Oct 2, 2026
e03d4ad
Recorded channel subscriptions and pending notifications on the workf…
moetemp Oct 2, 2026
1e95a66
Created a missing channel in its own transition before updating it.
moetemp Oct 2, 2026
0b8cbd4
Merge branch 'moe/AI-198-srv-6-reset-failover' into moe/AI-198-srv-7-…
moetemp Oct 2, 2026
5c63db0
Handed a new channel listener the latest notification.
moetemp Oct 2, 2026
4af5d32
Added the subscribe command and carried notifications on scheduled ev…
moetemp Oct 2, 2026
7e7efe1
Served the five notification channel RPCs on the frontend.
moetemp Oct 2, 2026
b77a9e8
Added functional tests for the notification channel.
moetemp Oct 2, 2026
caf94c1
Recorded the event for a repeated channel subscribe.
moetemp Oct 2, 2026
f4daadd
Answered a notify that changes nothing without a write.
moetemp Oct 2, 2026
06c42d4
Folded a repeated notification per listener, not per channel.
moetemp Oct 2, 2026
cede30e
Folded a repeated notification into a task that has not started.
moetemp Oct 2, 2026
fae6eba
Merge branch 'moe/AI-198-srv-6-reset-failover' into moe/AI-198-srv-7-…
moetemp Oct 2, 2026
c893b6e
Merge branch 'moe/AI-198-srv-6-reset-failover' into moe/AI-198-srv-7-…
moetemp Oct 2, 2026
93bc380
Gave each regenerated test timer a distinct deadline.
moetemp Oct 2, 2026
c44b0f7
Merge branch 'moe/AI-198-srv-6-reset-failover' into moe/AI-198-srv-7-…
moetemp Oct 2, 2026
639b633
Merge branch 'moe/AI-198-srv-6-reset-failover' into moe/AI-198-srv-7-…
moetemp Oct 3, 2026
655c9a0
Merge branch 'moe/AI-198-srv-6-reset-failover' into moe/AI-198-srv-7-…
moetemp Oct 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
780 changes: 780 additions & 0 deletions chasm/lib/channel/channel.go

Large diffs are not rendered by default.

415 changes: 415 additions & 0 deletions chasm/lib/channel/channel_test.go

Large diffs are not rendered by default.

138 changes: 138 additions & 0 deletions chasm/lib/channel/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package channel

import (
"time"

"go.temporal.io/server/common/dynamicconfig"
)

// MaxPositionBytes bounds the position a notification carries. It is a cursor
// for the listener to read from rather than data, and the channel retains it.
const MaxPositionBytes = 1024

// LongPollTimeout matches the other history long polls: on expiry the caller
// gets an empty response and polls again.
const LongPollTimeout = 20 * time.Second

// LongPollBuffer leaves room to answer before the caller's own deadline.
const LongPollBuffer = 3 * time.Second

// RoutedCallTimeout bounds one call to another shard, so a slow listener does
// not take the whole budget of the task that is telling the others.
const RoutedCallTimeout = 5 * time.Second

// The defaults below are also what component code falls back on when it is
// driven without a config, as the unit tests do.
const (
DefaultMaxListeners = 1000
DefaultRetainedNotifications = 1000
DefaultMaxMetadataBytes = 2048
DefaultRetention = time.Hour
DefaultNotifyPerSecond = 10000
DefaultMaxSubscriptionsPerWorkflow = 32
)

var (
MaxListenersSetting = dynamicconfig.NewNamespaceIntSetting(
"channel.maxListeners",
DefaultMaxListeners,
`Most listeners, workflow and callback together, one notification channel holds. A
registration past it is refused.`,
)
RetainedNotificationsSetting = dynamicconfig.NewNamespaceIntSetting(
"channel.retainedNotifications",
DefaultRetainedNotifications,
`Most notifications a channel keeps for pollers. The oldest goes when a new one
arrives past it.`,
)
MaxMetadataBytesSetting = dynamicconfig.NewNamespaceIntSetting(
"channel.maxMetadataBytes",
DefaultMaxMetadataBytes,
`Largest notification metadata accepted, summed over its payloads.`,
)
RetentionSetting = dynamicconfig.NewNamespaceDurationSetting(
"channel.retention",
DefaultRetention,
`How long a channel with no listeners stays after its last notify, registration or
poll before it is deleted with what it retained.`,
)
NotifyPerSecondSetting = dynamicconfig.NewNamespaceIntSetting(
"channel.notifyPerSecond",
DefaultNotifyPerSecond,
`Most NotifyChannel calls a namespace may make per second on one history host. Zero
means no limit.`,
)
MaxSubscriptionsPerWorkflowSetting = dynamicconfig.NewNamespaceIntSetting(
"channel.maxSubscriptionsPerWorkflow",
DefaultMaxSubscriptionsPerWorkflow,
`Most notification channels one workflow run subscribes to. A subscribe command past
it fails the workflow task.`,
)
)

// Limits are the per-namespace bounds a channel transition applies, resolved
// by the caller so the component needs no config of its own.
type Limits struct {
MaxListeners int
RetainedNotifications int
MaxMetadataBytes int
Retention time.Duration
}

// DefaultLimits is what applies when nothing is configured.
func DefaultLimits() Limits {
return Limits{
MaxListeners: DefaultMaxListeners,
RetainedNotifications: DefaultRetainedNotifications,
MaxMetadataBytes: DefaultMaxMetadataBytes,
Retention: DefaultRetention,
}
}

// withDefaults fills what a caller left at zero.
func (l Limits) withDefaults() Limits {
d := DefaultLimits()
if l.MaxListeners <= 0 {
l.MaxListeners = d.MaxListeners
}
if l.RetainedNotifications <= 0 {
l.RetainedNotifications = d.RetainedNotifications
}
if l.MaxMetadataBytes <= 0 {
l.MaxMetadataBytes = d.MaxMetadataBytes
}
if l.Retention <= 0 {
l.Retention = d.Retention
}
return l
}

type Config struct {
MaxListeners dynamicconfig.IntPropertyFnWithNamespaceFilter
RetainedNotifications dynamicconfig.IntPropertyFnWithNamespaceFilter
MaxMetadataBytes dynamicconfig.IntPropertyFnWithNamespaceFilter
Retention dynamicconfig.DurationPropertyFnWithNamespaceFilter
NotifyPerSecond dynamicconfig.IntPropertyFnWithNamespaceFilter
MaxSubscriptionsPerWorkflow dynamicconfig.IntPropertyFnWithNamespaceFilter
}

func NewConfig(dc *dynamicconfig.Collection) *Config {
return &Config{
MaxListeners: MaxListenersSetting.Get(dc),
RetainedNotifications: RetainedNotificationsSetting.Get(dc),
MaxMetadataBytes: MaxMetadataBytesSetting.Get(dc),
Retention: RetentionSetting.Get(dc),
NotifyPerSecond: NotifyPerSecondSetting.Get(dc),
MaxSubscriptionsPerWorkflow: MaxSubscriptionsPerWorkflowSetting.Get(dc),
}
}

// LimitsFor resolves the namespace's limits.
func (c *Config) LimitsFor(namespaceName string) Limits {
return Limits{
MaxListeners: c.MaxListeners(namespaceName),
RetainedNotifications: c.RetainedNotifications(namespaceName),
MaxMetadataBytes: c.MaxMetadataBytes(namespaceName),
Retention: c.Retention(namespaceName),
}
}
71 changes: 71 additions & 0 deletions chasm/lib/channel/convert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package channel

import (
notificationpb "go.temporal.io/api/notification/v1"
channelpb "go.temporal.io/server/chasm/lib/channel/gen/channelpb/v1"
"go.temporal.io/server/common"
)

// The stored notification has the public message's fields under the same
// names, so converting is a field copy in either direction.

func FromAPINotification(n *notificationpb.Notification) *channelpb.Notification {
if n == nil {
return nil
}
n = common.CloneProto(n)
return &channelpb.Notification{
Channel: n.GetChannel(),
Position: n.GetPosition(),
Counter: n.GetCounter(),
Metadata: n.GetMetadata(),
}
}

func ToAPINotification(n *channelpb.Notification) *notificationpb.Notification {
if n == nil {
return nil
}
n = common.CloneProto(n)
return &notificationpb.Notification{
Channel: n.GetChannel(),
Position: n.GetPosition(),
Counter: n.GetCounter(),
Metadata: n.GetMetadata(),
}
}

func ToAPINotifications(ns []*channelpb.Notification) []*notificationpb.Notification {
if len(ns) == 0 {
return nil
}
out := make([]*notificationpb.Notification, len(ns))
for i, n := range ns {
out[i] = ToAPINotification(n)
}
return out
}

// ToAPIListener converts a listener from describe.
func ToAPIListener(l *channelpb.ChannelListenerInfo) *notificationpb.ChannelListener {
out := &notificationpb.ChannelListener{
ListenerId: l.GetListenerId(),
RegisteredTime: common.CloneProto(l.GetRegisteredTime()),
}
switch variant := l.GetVariant().(type) {
case *channelpb.ChannelListenerInfo_Workflow_:
out.Listener = &notificationpb.ChannelListener_Workflow{
Workflow: &notificationpb.WorkflowListener{
WorkflowId: variant.Workflow.GetWorkflowId(),
RunId: variant.Workflow.GetRunId(),
},
}
case *channelpb.ChannelListenerInfo_Callback:
out.Listener = &notificationpb.ChannelListener_Callback{
Callback: common.CloneProto(variant.Callback),
}
default:
// A listener with no variant has nothing more to say.
}
return out
}
43 changes: 43 additions & 0 deletions chasm/lib/channel/gen/channelpb/v1/channel_state.go-helpers.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading