Files
tasks/common/persistence/cassandra/matching_task_store_v1.go
David ReissandGitHub 993e02a2bc Skip most metadata updates on append tasks in Cassandra (#9739)
## What changed?
For Cassandra matching tasks implementation, we always did a full
metadata update with creating new tasks. Change it to only update if
it's been 5s or more from the last update.

## Why?
Optimization: move less data through LWT coordination.

## How did you test it?
- [ ] built
- [ ] run locally and tested manually
- [ ] covered by existing tests
- [ ] added new unit test(s)
- [ ] added new functional test(s)

## Potential risks
We could lose up to 5s of approximate backlog count values on partition
move. When a partition isn't moving, backlog count API results and
metrics are based on the in-memory values, so no change there.
2026-04-22 14:57:09 -07:00

232 lines
6.4 KiB
Go

package cassandra
import (
"context"
"fmt"
"go.temporal.io/api/serviceerror"
p "go.temporal.io/server/common/persistence"
"go.temporal.io/server/common/persistence/nosql/nosqlplugin/cassandra/gocql"
)
const (
templateCreateTaskQuery = `INSERT INTO tasks (` +
`namespace_id, task_queue_name, task_queue_type, type, task_id, task, task_encoding) ` +
`VALUES(?, ?, ?, ?, ?, ?, ?)`
templateCreateTaskWithTTLQuery = `INSERT INTO tasks (` +
`namespace_id, task_queue_name, task_queue_type, type, task_id, task, task_encoding) ` +
`VALUES(?, ?, ?, ?, ?, ?, ?) USING TTL ?`
templateGetTasksQuery = `SELECT task_id, task, task_encoding ` +
`FROM tasks ` +
`WHERE namespace_id = ? ` +
`and task_queue_name = ? ` +
`and task_queue_type = ? ` +
`and type = ? ` +
`and task_id >= ? ` +
`and task_id < ?`
templateCompleteTasksLessThanQuery = `DELETE FROM tasks ` +
`WHERE namespace_id = ? ` +
`AND task_queue_name = ? ` +
`AND task_queue_type = ? ` +
`AND type = ? ` +
`AND task_id < ? `
)
type matchingTaskStoreV1 struct {
Session gocql.Session
userDataStore
taskQueueStore
}
func newMatchingTaskStoreV1(
session gocql.Session,
) *matchingTaskStoreV1 {
return &matchingTaskStoreV1{
Session: session,
userDataStore: userDataStore{Session: session},
taskQueueStore: taskQueueStore{Session: session, version: matchingTaskVersion1},
}
}
// CreateTasks add tasks
func (d *matchingTaskStoreV1) CreateTasks(
ctx context.Context,
request *p.InternalCreateTasksRequest,
) (*p.CreateTasksResponse, error) {
batch := d.Session.NewBatch(gocql.LoggedBatch).WithContext(ctx)
namespaceID := request.NamespaceID
taskQueue := request.TaskQueue
taskQueueType := request.TaskType
for _, task := range request.Tasks {
if task.TaskPass != 0 {
return nil, serviceerror.NewInternal("invalid non-fair queue task with pass number")
}
ttl := getTaskTTL(task.ExpiryTime)
if ttl <= 0 || ttl > maxCassandraTTL {
batch.Query(templateCreateTaskQuery,
namespaceID,
taskQueue,
taskQueueType,
rowTypeTaskInSubqueue(task.Subqueue),
task.TaskId,
task.Task.Data,
task.Task.EncodingType.String())
} else {
batch.Query(templateCreateTaskWithTTLQuery,
namespaceID,
taskQueue,
taskQueueType,
rowTypeTaskInSubqueue(task.Subqueue),
task.TaskId,
task.Task.Data,
task.Task.EncodingType.String(),
ttl)
}
}
// The following query is used to ensure that range_id didn't change.
// When UpdateMetadata is true, we also write the metadata blob (backlog counts, etc.).
// When false, we only check the range_id for write fencing.
if request.UpdateMetadata {
batch.Query(switchTasksTable(templateUpdateTaskQueueQuery, matchingTaskVersion1),
request.RangeID,
request.TaskQueueInfo.Data,
request.TaskQueueInfo.EncodingType.String(),
namespaceID,
taskQueue,
taskQueueType,
rowTypeTaskQueue,
taskQueueTaskID,
request.RangeID,
)
} else {
batch.Query(switchTasksTable(templateCheckRangeIDQuery, matchingTaskVersion1),
request.RangeID,
namespaceID,
taskQueue,
taskQueueType,
rowTypeTaskQueue,
taskQueueTaskID,
request.RangeID,
)
}
previous := make(map[string]any)
applied, _, err := d.Session.MapExecuteBatchCAS(batch, previous)
if err != nil {
return nil, gocql.ConvertError("CreateTasks", err)
}
if !applied {
rangeID := previous["range_id"]
return nil, &p.ConditionFailedError{
Msg: fmt.Sprintf("Failed to create task. TaskQueue: %v, taskQueueType: %v, rangeID: %v, db rangeID: %v",
taskQueue, taskQueueType, request.RangeID, rangeID),
}
}
return &p.CreateTasksResponse{UpdatedMetadata: request.UpdateMetadata}, nil
}
// GetTasks get a task
func (d *matchingTaskStoreV1) GetTasks(
ctx context.Context,
request *p.GetTasksRequest,
) (*p.InternalGetTasksResponse, error) {
if request.InclusiveMinPass != 0 {
return nil, serviceerror.NewInternal("invalid GetTasks request on queue: InclusiveMinPass is not supported")
}
// Reading taskqueue tasks need to be quorum level consistent, otherwise we could lose tasks
query := d.Session.Query(templateGetTasksQuery,
request.NamespaceID,
request.TaskQueue,
request.TaskType,
rowTypeTaskInSubqueue(request.Subqueue),
request.InclusiveMinTaskID,
request.ExclusiveMaxTaskID,
).WithContext(ctx)
iter := query.PageSize(request.PageSize).PageState(request.NextPageToken).Iter()
response := &p.InternalGetTasksResponse{}
task := make(map[string]any)
for iter.MapScan(task) {
_, ok := task["task_id"]
if !ok { // no tasks, but static column record returned
continue
}
rawTask, ok := task["task"]
if !ok {
return nil, newFieldNotFoundError("task", task)
}
taskVal, ok := rawTask.([]byte)
if !ok {
var byteSliceType []byte
return nil, newPersistedTypeMismatchError("task", byteSliceType, rawTask, task)
}
rawEncoding, ok := task["task_encoding"]
if !ok {
return nil, newFieldNotFoundError("task_encoding", task)
}
encodingVal, ok := rawEncoding.(string)
if !ok {
var byteSliceType []byte
return nil, newPersistedTypeMismatchError("task_encoding", byteSliceType, rawEncoding, task)
}
response.Tasks = append(response.Tasks, p.NewDataBlob(taskVal, encodingVal))
task = make(map[string]any) // Reinitialize map as initialized fails on unmarshalling
}
if len(iter.PageState()) > 0 {
response.NextPageToken = iter.PageState()
}
if err := iter.Close(); err != nil {
return nil, gocql.ConvertError("GetTasks", err)
}
return response, nil
}
// CompleteTasksLessThan deletes all tasks less than the given task id. This API ignores the
// Limit request parameter i.e. either all tasks leq the task_id will be deleted or an error will
// be returned to the caller
func (d *matchingTaskStoreV1) CompleteTasksLessThan(
ctx context.Context,
request *p.CompleteTasksLessThanRequest,
) (int, error) {
if request.ExclusiveMaxPass != 0 {
return 0, serviceerror.NewInternal("invalid CompleteTasksLessThan request on queue")
}
query := d.Session.Query(
templateCompleteTasksLessThanQuery,
request.NamespaceID,
request.TaskQueueName,
request.TaskType,
rowTypeTaskInSubqueue(request.Subqueue),
request.ExclusiveMaxTaskID,
).WithContext(ctx)
err := query.Exec()
if err != nil {
return 0, gocql.ConvertError("CompleteTasksLessThan", err)
}
return p.UnknownNumRowsAffected, nil
}
func (d *matchingTaskStoreV1) GetName() string {
return cassandraPersistenceName
}
func (d *matchingTaskStoreV1) Close() {
if d.Session != nil {
d.Session.Close()
}
}