go.temporal.io/server/service/matching/reachability.go
349 LOC · 168 covered · 181 uncovered · 49 ranges · 83 concepts · 16 introducers · 20 tests
File neighbourhood
The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.
Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file
In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.
Graph controls are ready.
Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.
package matching
import (
"context"
"fmt"
"slices"
"strings"
"time"
"github.com/temporalio/sqlparser"
enumspb "go.temporal.io/api/enums/v1"
clockspb "go.temporal.io/server/api/clock/v1"
persistencespb "go.temporal.io/server/api/persistence/v1"
"go.temporal.io/server/common/cache"
hlc "go.temporal.io/server/common/clock/hybrid_logical_clock"
"go.temporal.io/server/common/log"
"go.temporal.io/server/common/log/tag"
"go.temporal.io/server/common/metrics"
"go.temporal.io/server/common/namespace"
"go.temporal.io/server/common/persistence/visibility/manager"
"go.temporal.io/server/common/searchattribute/sadefs"
"go.temporal.io/server/common/tqid"
"go.temporal.io/server/common/util"
"go.temporal.io/server/common/worker_versioning"
)
const (
reachabilityCacheMaxSize = 10000
)
type reachabilityExitPoint int32
const (
checkedRuleSourcesForInput reachabilityExitPoint = 0
checkedRuleTargetsForUpstream reachabilityExitPoint = 1
checkedBacklogForUpstream reachabilityExitPoint = 2
checkedOpenWorkflowExecutionsForUpstreamHit reachabilityExitPoint = 3
checkedOpenWorkflowExecutionsForUpstreamMiss reachabilityExitPoint = 4
checkedClosedWorkflowExecutionsForUpstreamHit reachabilityExitPoint = 5
checkedClosedWorkflowExecutionsForUpstreamMiss reachabilityExitPoint = 6
reachabilityExitPointTagName = "reachability_exit_point"
)
var (
reachabilityExitPoint2TagValue = map[reachabilityExitPoint]string{
checkedRuleSourcesForInput: "checked_rule_sources_for_input",
checkedRuleTargetsForUpstream: "checked_rule_targets_for_upstream",
checkedBacklogForUpstream: "checked_backlog_for_upstream",
checkedOpenWorkflowExecutionsForUpstreamHit: "checked_open_wf_executions_for_upstream_hit",
checkedOpenWorkflowExecutionsForUpstreamMiss: "checked_open_wf_executions_for_upstream_miss",
checkedClosedWorkflowExecutionsForUpstreamHit: "checked_closed_wf_executions_for_upstream_hit",
checkedClosedWorkflowExecutionsForUpstreamMiss: "checked_closed_wf_executions_for_upstream_miss",
}
)
type reachabilityCalculator struct {
cache reachabilityCache
nsID namespace.ID
nsName namespace.Name
taskQueue *tqid.TaskQueueFamily
assignmentRules []*persistencespb.AssignmentRule
redirectRules []*persistencespb.RedirectRule
buildIdVisibilityGracePeriod time.Duration
tqConfig *taskQueueConfig
}
func newReachabilityCalculator(
data *persistencespb.VersioningData,
rCache reachabilityCache,
nsID, nsName string,
taskQueue *tqid.TaskQueueFamily,
buildIdVisibilityGracePeriod time.Duration,
tqConfig *taskQueueConfig,
) *reachabilityCalculator {
return &reachabilityCalculator{
cache: rCache,
nsID: namespace.ID(nsID),
nsName: namespace.Name(nsName),
taskQueue: taskQueue,
assignmentRules: data.GetAssignmentRules(),
redirectRules: data.GetRedirectRules(),
buildIdVisibilityGracePeriod: buildIdVisibilityGracePeriod,
tqConfig: tqConfig,
}
}
func getBuildIdTaskReachability(
ctx context.Context,
rc *reachabilityCalculator,
metricsHandler metrics.Handler,
logger log.Logger,
buildId string,
reachability, exitPoint, err := rc.run(ctx, buildId)
handler := metrics.GetPerTaskQueueFamilyScope(metricsHandler, rc.nsName.String(), rc.taskQueue, rc.tqConfig.BreakdownMetricsByTaskQueue())
metrics.ReachabilityExitPointCounter.With(handler).Record(1,
metrics.WorkerVersionTag(buildId, rc.tqConfig.BreakdownMetricsByBuildID()),
metrics.StringTag(reachabilityExitPointTagName, reachabilityExitPoint2TagValue[exitPoint]))
logger.Info("Calculated reachability for build id",
tag.WorkerVersion(buildId),
tag.BuildIdTaskReachabilityTag(reachability.String()),
tag.ReachabilityExitPointTag(reachabilityExitPoint2TagValue[exitPoint]),
tag.WorkflowNamespace(rc.nsName.String()),
tag.WorkflowTaskQueueName(rc.taskQueue.Name()),
)
return reachability, err
}
func (rc *reachabilityCalculator) run(ctx context.Context, buildId string) (enumspb.BuildIdTaskReachability, reachabilityExitPoint, error) {
reachability.go ×18
// 1. Easy UNREACHABLE case
if isActiveRedirectRuleSource(buildId, rc.redirectRules) {
return enumspb.BUILD_ID_TASK_REACHABILITY_UNREACHABLE, checkedRuleSourcesForInput, nil
reachability.go ×1
}
// Gather list of all build ids that could point to buildId
// 2. Cases for REACHABLE
// 2a. If buildId is assignable to new tasks
if slices.ContainsFunc(buildIdsOfInterest, rc.isReachableActiveAssignmentRuleTargetOrDefault) {
return enumspb.BUILD_ID_TASK_REACHABILITY_REACHABLE, checkedRuleTargetsForUpstream, nil
reachability.go ×1
}
// 2b. If buildId could be reached from the backlog
if existsBacklog, err := rc.existsBackloggedActivityOrWFTaskAssignedToAny(ctx, buildIdsOfInterest); err != nil {
reachability.go ×18
return enumspb.BUILD_ID_TASK_REACHABILITY_UNSPECIFIED, checkedBacklogForUpstream, err
return enumspb.BUILD_ID_TASK_REACHABILITY_REACHABLE, checkedBacklogForUpstream, nil
}
// Note: The below cases are not applicable to activity-only task queues, since we don't record those in visibility
// Gather list of all build ids that could point to buildId, now including deleted rules to account for the delay in updating visibility
buildIdsOfInterest = rc.getBuildIdsOfInterest(buildId, rc.buildIdVisibilityGracePeriod)
reachability.go ×18
// 2c. If buildId is assignable to tasks from open workflows
existsOpenWFAssignedToBuildId, hit, err := rc.existsWFAssignedToAny(ctx, buildIdsOfInterest, true)
if err != nil {
return enumspb.BUILD_ID_TASK_REACHABILITY_UNSPECIFIED, checkedOpenWorkflowExecutionsForUpstreamMiss, err
}
return enumspb.BUILD_ID_TASK_REACHABILITY_REACHABLE, getCacheExitPoint(true, hit), nil
reachability.go ×3
}
// 3. Cases for CLOSED_WORKFLOWS_ONLY
existsClosedWFAssignedToBuildId, hit, err := rc.existsWFAssignedToAny(ctx, buildIdsOfInterest, false)
reachability.go ×18
if err != nil {
return enumspb.BUILD_ID_TASK_REACHABILITY_UNSPECIFIED, checkedClosedWorkflowExecutionsForUpstreamMiss, err
}
return enumspb.BUILD_ID_TASK_REACHABILITY_CLOSED_WORKFLOWS_ONLY, getCacheExitPoint(false, hit), nil
reachability.go ×3
}
// 4. Otherwise, UNREACHABLE
return enumspb.BUILD_ID_TASK_REACHABILITY_UNREACHABLE, getCacheExitPoint(false, hit), nil
reachability.go ×1
}
if open {
return checkedOpenWorkflowExecutionsForUpstreamHit
}
return checkedOpenWorkflowExecutionsForUpstreamMiss
}
return checkedClosedWorkflowExecutionsForUpstreamHit
}
return checkedClosedWorkflowExecutionsForUpstreamMiss
}
// getBuildIdsOfInterest returns a list of build ids that point to the given buildId in the graph
// of redirect rules and adds the given build ID to that list.
// It considers rules if the deletion time is nil or within the given deletedRuleInclusionPeriod.
func (rc *reachabilityCalculator) getBuildIdsOfInterest(
buildId string,
withinRuleInclusionPeriod := func(clk *clockspb.HybridLogicalClock) bool {
return true
}
}
includedRules := util.FilterSlice(slices.Clone(rc.redirectRules), func(rr *persistencespb.RedirectRule) bool {
reachability.go ×3
return rr.DeleteTimestamp == nil || withinRuleInclusionPeriod(rr.DeleteTimestamp)
reachability.go ×1
})
}
func (rc *reachabilityCalculator) existsBackloggedActivityOrWFTaskAssignedToAny(ctx context.Context, buildIdsOfInterest []string) (bool, error) {
reachability.go ×18
// todo backlog
return false, nil
}
func (rc *reachabilityCalculator) isReachableActiveAssignmentRuleTargetOrDefault(buildId string) bool {
reachability.go ×3
foundFullyRampedRule := false
for _, r := range getActiveAssignmentRules(rc.assignmentRules) {
}
// rules after a fully-ramped rule will not be reached
foundFullyRampedRule = true
break
}
}
return true
}
}
func (rc *reachabilityCalculator) existsWFAssignedToAny(
ctx context.Context,
buildIdsOfInterest []string,
open bool,
query := rc.makeBuildIdQuery(buildIdsOfInterest, open)
return rc.cache.Get(ctx, *rc.makeBuildIdCountRequest(query), open)
}
func (rc *reachabilityCalculator) makeBuildIdCountRequest(query string) *manager.CountWorkflowExecutionsRequest {
reachability.go ×18
return &manager.CountWorkflowExecutionsRequest{
NamespaceID: rc.nsID,
Namespace: rc.nsName,
Query: query,
}
}
func (rc *reachabilityCalculator) makeBuildIdQuery(
buildIdsOfInterest []string,
open bool,
slices.Sort(buildIdsOfInterest)
escapedTaskQueue := sqlparser.String(sqlparser.NewStrVal([]byte(rc.taskQueue.Name())))
var statusFilter string
var escapedBuildIds []string
var includeNull bool
if open {
statusFilter = fmt.Sprintf(` AND %s = "Running"`, sadefs.ExecutionStatus)
// want: currently assigned to that build-id
// (b1, b2) --> (assigned:b1, assigned:b2)
// (b1, b2, "") --> (assigned:b1, assigned:b2, unversioned, null)
// ("") --> (unversioned, null)
for _, bid := range buildIdsOfInterest {
if bid == "" {
escapedBuildIds = append(escapedBuildIds, sqlparser.String(sqlparser.NewStrVal([]byte(worker_versioning.UnversionedSearchAttribute))))
reachability.go ×3
includeNull = true
escapedBuildIds = append(escapedBuildIds, sqlparser.String(sqlparser.NewStrVal([]byte(worker_versioning.AssignedBuildIdSearchAttribute(bid)))))
}
}
statusFilter = fmt.Sprintf(` AND %s != "Running"`, sadefs.ExecutionStatus)
// want: closed AT that build ID, and once used that build ID
// (b1, b2) --> (versioned:b1, versioned:b2)
// (b1, b2, "") --> (versioned:b1, versioned:b2, unversioned, null)
// ("") --> (unversioned, null)
for _, bid := range buildIdsOfInterest {
if bid == "" {
escapedBuildIds = append(escapedBuildIds, sqlparser.String(sqlparser.NewStrVal([]byte(worker_versioning.UnversionedSearchAttribute))))
reachability.go ×3
includeNull = true
escapedBuildIds = append(escapedBuildIds, sqlparser.String(sqlparser.NewStrVal([]byte(worker_versioning.VersionedBuildIdSearchAttribute(bid)))))
}
}
}
buildIdsFilter := fmt.Sprintf("%s IN (%s)", sadefs.BuildIds, strings.Join(escapedBuildIds, ","))
reachability.go ×6
if includeNull {
buildIdsFilter = fmt.Sprintf("(%s IS NULL OR %s)", sadefs.BuildIds, buildIdsFilter)
reachability.go ×3
}
return fmt.Sprintf("%s = %s AND %s%s", sadefs.TaskQueue, escapedTaskQueue, buildIdsFilter, statusFilter)
reachability.go ×6
}
// getDefaultBuildId gets the build ID mentioned in the first fully-ramped Assignment Rule.
// If there is no default Build ID, the result for the unversioned queue will be returned.
// This should only be called on the root.
func getDefaultBuildId(assignmentRules []*persistencespb.AssignmentRule) string {
reachability.go ×2
for _, ar := range getActiveAssignmentRules(assignmentRules) {
if isFullyRamped(ar.GetRule()) {
return ar.GetRule().GetTargetBuildId()
}
}
}
/*
In-memory Reachability Cache of Visibility Queries and Results
*/
type reachabilityCache struct {
openWFCache cache.Cache
closedWFCache cache.Cache // these are separate due to allow for different TTL
metricsHandler metrics.Handler
visibilityMgr manager.VisibilityManager
}
func newReachabilityCache(
handler metrics.Handler,
visibilityMgr manager.VisibilityManager,
reachabilityCacheOpenWFExecutionTTL,
reachabilityCacheClosedWFExecutionTTL time.Duration,
return reachabilityCache{
openWFCache: cache.New(reachabilityCacheMaxSize, &cache.Options{TTL: reachabilityCacheOpenWFExecutionTTL}),
closedWFCache: cache.New(reachabilityCacheMaxSize, &cache.Options{TTL: reachabilityCacheClosedWFExecutionTTL}),
metricsHandler: handler,
visibilityMgr: visibilityMgr,
}
}
// Get retrieves the Workflow Count existence value based on the query-string key.
func (c *reachabilityCache) Get(ctx context.Context, countRequest manager.CountWorkflowExecutionsRequest, open bool) (exists, hit bool, err error) {
reachability.go ×18
// try cache
var result any
if open {
result = c.openWFCache.Get(countRequest)
} else {
result = c.closedWFCache.Get(countRequest)
}
if result != nil {
// there's no reason that the cache would ever contain a non-bool, but just in case, treat non-bool as a miss
exists, ok := result.(bool)
if ok {
return exists, true, nil
}
}
// cache was cold, ask visibility and put result in cache
countResponse, err := c.visibilityMgr.CountWorkflowExecutions(ctx, &countRequest)
reachability.go ×18
if err != nil {
return false, false, err
}
c.Put(countRequest, exists, open)
return exists, false, nil
}
// Put adds an element to the cache.
func (c *reachabilityCache) Put(countRequest manager.CountWorkflowExecutionsRequest, exists, open bool) {
reachability.go ×18
if open {
c.openWFCache.Put(countRequest, exists)
} else {
c.closedWFCache.Put(countRequest, exists)
}
}