Files
tasks/common/persistence/cassandra/matching_task_store.go
Alex ShtinandGitHub 846d88c8cb Refactor: use new serviceerror.New...f constructors (#7785)
## What changed?
Refactor: use new `serviceerror.New...f` constructors.

## Why?
Most of service errors now supports `fmt.Sprintf` format and they should
be used instead of `fmt.Sprintf`.

## How did you test it?
- [ ] built
- [ ] run locally and tested manually
- [x] covered by existing tests
- [ ] added new unit test(s)
- [ ] added new functional test(s)
2025-05-17 00:21:32 +00:00

671 lines
20 KiB
Go

package cassandra
import (
"context"
"fmt"
"strings"
"time"
enumspb "go.temporal.io/api/enums/v1"
"go.temporal.io/api/serviceerror"
"go.temporal.io/server/common/convert"
"go.temporal.io/server/common/log"
p "go.temporal.io/server/common/persistence"
"go.temporal.io/server/common/persistence/nosql/nosqlplugin/cassandra/gocql"
"go.temporal.io/server/common/primitives/timestamp"
"google.golang.org/protobuf/types/known/timestamppb"
)
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 < ? `
templateGetTaskQueueQuery = `SELECT ` +
`range_id, ` +
`task_queue, ` +
`task_queue_encoding ` +
`FROM tasks ` +
`WHERE namespace_id = ? ` +
`and task_queue_name = ? ` +
`and task_queue_type = ? ` +
`and type = ? ` +
`and task_id = ?`
templateInsertTaskQueueQuery = `INSERT INTO tasks (` +
`namespace_id, ` +
`task_queue_name, ` +
`task_queue_type, ` +
`type, ` +
`task_id, ` +
`range_id, ` +
`task_queue, ` +
`task_queue_encoding ` +
`) VALUES (?, ?, ?, ?, ?, ?, ?, ?) IF NOT EXISTS`
templateUpdateTaskQueueQuery = `UPDATE tasks SET ` +
`range_id = ?, ` +
`task_queue = ?, ` +
`task_queue_encoding = ? ` +
`WHERE namespace_id = ? ` +
`and task_queue_name = ? ` +
`and task_queue_type = ? ` +
`and type = ? ` +
`and task_id = ? ` +
`IF range_id = ?`
templateUpdateTaskQueueQueryWithTTLPart1 = `INSERT INTO tasks (` +
`namespace_id, ` +
`task_queue_name, ` +
`task_queue_type, ` +
`type, ` +
`task_id ` +
`) VALUES (?, ?, ?, ?, ?) USING TTL ?`
templateUpdateTaskQueueQueryWithTTLPart2 = `UPDATE tasks USING TTL ? SET ` +
`range_id = ?, ` +
`task_queue = ?, ` +
`task_queue_encoding = ? ` +
`WHERE namespace_id = ? ` +
`and task_queue_name = ? ` +
`and task_queue_type = ? ` +
`and type = ? ` +
`and task_id = ? ` +
`IF range_id = ?`
templateDeleteTaskQueueQuery = `DELETE FROM tasks ` +
`WHERE namespace_id = ? ` +
`AND task_queue_name = ? ` +
`AND task_queue_type = ? ` +
`AND type = ? ` +
`AND task_id = ? ` +
`IF range_id = ?`
templateGetTaskQueueUserDataQuery = `SELECT data, data_encoding, version
FROM task_queue_user_data
WHERE namespace_id = ? AND build_id = ''
AND task_queue_name = ?`
templateUpdateTaskQueueUserDataQuery = `UPDATE task_queue_user_data SET
data = ?,
data_encoding = ?,
version = ?
WHERE namespace_id = ?
AND build_id = ''
AND task_queue_name = ?
IF version = ?`
templateInsertTaskQueueUserDataQuery = `INSERT INTO task_queue_user_data
(namespace_id, build_id, task_queue_name, data, data_encoding, version) VALUES
(? , '' , ? , ? , ? , 1 ) IF NOT EXISTS`
templateInsertBuildIdTaskQueueMappingQuery = `INSERT INTO task_queue_user_data
(namespace_id, build_id, task_queue_name) VALUES
(? , ? , ?)`
templateDeleteBuildIdTaskQueueMappingQuery = `DELETE FROM task_queue_user_data
WHERE namespace_id = ? AND build_id = ? AND task_queue_name = ?`
templateListTaskQueueUserDataQuery = `SELECT task_queue_name, data, data_encoding, version FROM task_queue_user_data WHERE namespace_id = ? AND build_id = ''`
templateListTaskQueueNamesByBuildIdQuery = `SELECT task_queue_name FROM task_queue_user_data WHERE namespace_id = ? AND build_id = ?`
templateCountTaskQueueByBuildIdQuery = `SELECT COUNT(*) FROM task_queue_user_data WHERE namespace_id = ? AND build_id = ?`
// Not much of a need to make this configurable, we're just reading some strings
listTaskQueueNamesByBuildIdPageSize = 100
)
const (
// Row types for table tasks. Lower bit only: see rowTypeTaskInSubqueue for more details.
rowTypeTask = iota
rowTypeTaskQueue
)
// We steal some upper bits of the "row type" field to hold a subqueue index.
// Subqueue 0 must be the same as rowTypeTask (before subqueues were introduced).
// 00000000: task in subqueue 0 (rowTypeTask)
// 00000001: task queue metadata (rowTypeTaskQueue)
// xxxxxx1x: reserved
// 00000100: task in subqueue 1
// nnnnnn00: task in subqueue n, etc.
func rowTypeTaskInSubqueue(subqueue int) int {
return subqueue<<2 | rowTypeTask // nolint:staticcheck
}
type (
MatchingTaskStore struct {
Session gocql.Session
Logger log.Logger
}
)
func NewMatchingTaskStore(
session gocql.Session,
logger log.Logger,
) *MatchingTaskStore {
return &MatchingTaskStore{
Session: session,
Logger: logger,
}
}
func (d *MatchingTaskStore) CreateTaskQueue(
ctx context.Context,
request *p.InternalCreateTaskQueueRequest,
) error {
query := d.Session.Query(templateInsertTaskQueueQuery,
request.NamespaceID,
request.TaskQueue,
request.TaskType,
rowTypeTaskQueue,
taskQueueTaskID,
request.RangeID,
request.TaskQueueInfo.Data,
request.TaskQueueInfo.EncodingType.String(),
).WithContext(ctx)
previous := make(map[string]interface{})
applied, err := query.MapScanCAS(previous)
if err != nil {
return gocql.ConvertError("CreateTaskQueue", err)
}
if !applied {
previousRangeID := previous["range_id"]
return &p.ConditionFailedError{
Msg: fmt.Sprintf("CreateTaskQueue: TaskQueue:%v, TaskQueueType:%v, PreviousRangeID:%v",
request.TaskQueue, request.TaskType, previousRangeID),
}
}
return nil
}
func (d *MatchingTaskStore) GetTaskQueue(
ctx context.Context,
request *p.InternalGetTaskQueueRequest,
) (*p.InternalGetTaskQueueResponse, error) {
query := d.Session.Query(templateGetTaskQueueQuery,
request.NamespaceID,
request.TaskQueue,
request.TaskType,
rowTypeTaskQueue,
taskQueueTaskID,
).WithContext(ctx)
var rangeID int64
var tlBytes []byte
var tlEncoding string
if err := query.Scan(&rangeID, &tlBytes, &tlEncoding); err != nil {
return nil, gocql.ConvertError("GetTaskQueue", err)
}
return &p.InternalGetTaskQueueResponse{
RangeID: rangeID,
TaskQueueInfo: p.NewDataBlob(tlBytes, tlEncoding),
}, nil
}
// UpdateTaskQueue update task queue
func (d *MatchingTaskStore) UpdateTaskQueue(
ctx context.Context,
request *p.InternalUpdateTaskQueueRequest,
) (*p.UpdateTaskQueueResponse, error) {
var err error
var applied bool
previous := make(map[string]interface{})
if request.TaskQueueKind == enumspb.TASK_QUEUE_KIND_STICKY { // if task_queue is sticky, then update with TTL
if request.ExpiryTime == nil {
return nil, serviceerror.NewInternal("ExpiryTime cannot be nil for sticky task queue")
}
expiryTTL := convert.Int64Ceil(time.Until(timestamp.TimeValue(request.ExpiryTime)).Seconds())
if expiryTTL >= maxCassandraTTL {
expiryTTL = maxCassandraTTL
}
batch := d.Session.NewBatch(gocql.LoggedBatch).WithContext(ctx)
batch.Query(templateUpdateTaskQueueQueryWithTTLPart1,
request.NamespaceID,
request.TaskQueue,
request.TaskType,
rowTypeTaskQueue,
taskQueueTaskID,
expiryTTL,
)
batch.Query(templateUpdateTaskQueueQueryWithTTLPart2,
expiryTTL,
request.RangeID,
request.TaskQueueInfo.Data,
request.TaskQueueInfo.EncodingType.String(),
request.NamespaceID,
request.TaskQueue,
request.TaskType,
rowTypeTaskQueue,
taskQueueTaskID,
request.PrevRangeID,
)
applied, _, err = d.Session.MapExecuteBatchCAS(batch, previous)
} else {
query := d.Session.Query(templateUpdateTaskQueueQuery,
request.RangeID,
request.TaskQueueInfo.Data,
request.TaskQueueInfo.EncodingType.String(),
request.NamespaceID,
request.TaskQueue,
request.TaskType,
rowTypeTaskQueue,
taskQueueTaskID,
request.PrevRangeID,
).WithContext(ctx)
applied, err = query.MapScanCAS(previous)
}
if err != nil {
return nil, gocql.ConvertError("UpdateTaskQueue", err)
}
if !applied {
var columns []string
for k, v := range previous {
columns = append(columns, fmt.Sprintf("%s=%v", k, v))
}
return nil, &p.ConditionFailedError{
Msg: fmt.Sprintf("Failed to update task queue. name: %v, type: %v, rangeID: %v, columns: (%v)",
request.TaskQueue, request.TaskType, request.RangeID, strings.Join(columns, ",")),
}
}
return &p.UpdateTaskQueueResponse{}, nil
}
func (d *MatchingTaskStore) ListTaskQueue(
_ context.Context,
_ *p.ListTaskQueueRequest,
) (*p.InternalListTaskQueueResponse, error) {
return nil, serviceerror.NewUnavailable("unsupported operation")
}
func (d *MatchingTaskStore) DeleteTaskQueue(
ctx context.Context,
request *p.DeleteTaskQueueRequest,
) error {
query := d.Session.Query(
templateDeleteTaskQueueQuery,
request.TaskQueue.NamespaceID,
request.TaskQueue.TaskQueueName,
request.TaskQueue.TaskQueueType,
rowTypeTaskQueue,
taskQueueTaskID,
request.RangeID,
).WithContext(ctx)
previous := make(map[string]interface{})
applied, err := query.MapScanCAS(previous)
if err != nil {
return gocql.ConvertError("DeleteTaskQueue", err)
}
if !applied {
return &p.ConditionFailedError{
Msg: fmt.Sprintf("DeleteTaskQueue operation failed: expected_range_id=%v but found %+v", request.RangeID, previous),
}
}
return nil
}
// CreateTasks add tasks
func (d *MatchingTaskStore) 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 {
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
batch.Query(templateUpdateTaskQueueQuery,
request.RangeID,
request.TaskQueueInfo.Data,
request.TaskQueueInfo.EncodingType.String(),
namespaceID,
taskQueue,
taskQueueType,
rowTypeTaskQueue,
taskQueueTaskID,
request.RangeID,
)
previous := make(map[string]interface{})
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: true}, nil
}
func GetTaskTTL(expireTime *timestamppb.Timestamp) int64 {
var ttl int64 = 0
if expireTime != nil && !expireTime.AsTime().IsZero() {
expiryTtl := convert.Int64Ceil(time.Until(expireTime.AsTime()).Seconds())
// 0 means no ttl, we dont want that.
// Todo: Come back and correctly ignore expired in-memory tasks before persisting
if expiryTtl < 1 {
expiryTtl = 1
}
ttl = expiryTtl
}
return ttl
}
// GetTasks get a task
func (d *MatchingTaskStore) GetTasks(
ctx context.Context,
request *p.GetTasksRequest,
) (*p.InternalGetTasksResponse, error) {
// 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]interface{})
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]interface{}) // Reinitialize map as initialized fails on unmarshalling
}
if len(iter.PageState()) > 0 {
response.NextPageToken = iter.PageState()
}
if err := iter.Close(); err != nil {
return nil, serviceerror.NewUnavailablef("GetTasks operation failed. Error: %v", 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 *MatchingTaskStore) CompleteTasksLessThan(
ctx context.Context,
request *p.CompleteTasksLessThanRequest,
) (int, error) {
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 *MatchingTaskStore) GetTaskQueueUserData(
ctx context.Context,
request *p.GetTaskQueueUserDataRequest,
) (*p.InternalGetTaskQueueUserDataResponse, error) {
query := d.Session.Query(templateGetTaskQueueUserDataQuery,
request.NamespaceID,
request.TaskQueue,
).WithContext(ctx)
var version int64
var userDataBytes []byte
var encoding string
if err := query.Scan(&userDataBytes, &encoding, &version); err != nil {
return nil, gocql.ConvertError("GetTaskQueueData", err)
}
return &p.InternalGetTaskQueueUserDataResponse{
Version: version,
UserData: p.NewDataBlob(userDataBytes, encoding),
}, nil
}
func (d *MatchingTaskStore) UpdateTaskQueueUserData(
ctx context.Context,
request *p.InternalUpdateTaskQueueUserDataRequest,
) error {
batch := d.Session.NewBatch(gocql.UnloggedBatch).WithContext(ctx)
for taskQueue, update := range request.Updates {
if update.Version == 0 {
batch.Query(templateInsertTaskQueueUserDataQuery,
request.NamespaceID,
taskQueue,
update.UserData.Data,
update.UserData.EncodingType.String(),
)
} else {
batch.Query(templateUpdateTaskQueueUserDataQuery,
update.UserData.Data,
update.UserData.EncodingType.String(),
update.Version+1,
request.NamespaceID,
taskQueue,
update.Version,
)
}
for _, buildId := range update.BuildIdsAdded {
batch.Query(templateInsertBuildIdTaskQueueMappingQuery, request.NamespaceID, buildId, taskQueue)
}
for _, buildId := range update.BuildIdsRemoved {
batch.Query(templateDeleteBuildIdTaskQueueMappingQuery, request.NamespaceID, buildId, taskQueue)
}
}
previous := make(map[string]any)
applied, iter, err := d.Session.MapExecuteBatchCAS(batch, previous)
for _, update := range request.Updates {
if update.Applied != nil {
*update.Applied = applied
}
}
if err != nil {
return gocql.ConvertError("UpdateTaskQueueUserData", err)
}
defer iter.Close()
if !applied {
// No error, but not applied. That means we had a conflict.
// Iterate through results to identify first conflicting row.
for {
name, nameErr := getTypedFieldFromRow[string]("task_queue_name", previous)
previousVersion, verErr := getTypedFieldFromRow[int64]("version", previous)
update, hasUpdate := request.Updates[name]
if nameErr == nil && verErr == nil && hasUpdate && update.Version != previousVersion {
if update.Conflicting != nil {
*update.Conflicting = true
}
return &p.ConditionFailedError{
Msg: fmt.Sprintf("Failed to update task queues: task queue %q version %d != %d",
name, update.Version, previousVersion),
}
}
clear(previous)
if !iter.MapScan(previous) {
break
}
}
return &p.ConditionFailedError{Msg: "Failed to update task queues: unknown conflict"}
}
return nil
}
func (d *MatchingTaskStore) ListTaskQueueUserDataEntries(ctx context.Context, request *p.ListTaskQueueUserDataEntriesRequest) (*p.InternalListTaskQueueUserDataEntriesResponse, error) {
query := d.Session.Query(templateListTaskQueueUserDataQuery, request.NamespaceID).WithContext(ctx)
iter := query.PageSize(request.PageSize).PageState(request.NextPageToken).Iter()
response := &p.InternalListTaskQueueUserDataEntriesResponse{}
row := make(map[string]interface{})
for iter.MapScan(row) {
taskQueue, err := getTypedFieldFromRow[string]("task_queue_name", row)
if err != nil {
return nil, err
}
data, err := getTypedFieldFromRow[[]byte]("data", row)
if err != nil {
return nil, err
}
dataEncoding, err := getTypedFieldFromRow[string]("data_encoding", row)
if err != nil {
return nil, err
}
version, err := getTypedFieldFromRow[int64]("version", row)
if err != nil {
return nil, err
}
response.Entries = append(response.Entries, p.InternalTaskQueueUserDataEntry{TaskQueue: taskQueue, Data: p.NewDataBlob(data, dataEncoding), Version: version})
row = make(map[string]interface{}) // Reinitialize map as initialized fails on unmarshalling
}
if len(iter.PageState()) > 0 {
response.NextPageToken = iter.PageState()
}
if err := iter.Close(); err != nil {
return nil, serviceerror.NewUnavailablef("ListTaskQueueUserDataEntries operation failed. Error: %v", err)
}
return response, nil
}
func (d *MatchingTaskStore) GetTaskQueuesByBuildId(ctx context.Context, request *p.GetTaskQueuesByBuildIdRequest) ([]string, error) {
query := d.Session.Query(templateListTaskQueueNamesByBuildIdQuery, request.NamespaceID, request.BuildID).WithContext(ctx)
iter := query.PageSize(listTaskQueueNamesByBuildIdPageSize).Iter()
var taskQueues []string
row := make(map[string]interface{})
for {
for iter.MapScan(row) {
taskQueueRaw, ok := row["task_queue_name"]
if !ok {
return nil, newFieldNotFoundError("task_queue_name", row)
}
taskQueue, ok := taskQueueRaw.(string)
if !ok {
var stringType string
return nil, newPersistedTypeMismatchError("task_queue_name", stringType, taskQueueRaw, row)
}
taskQueues = append(taskQueues, taskQueue)
row = make(map[string]interface{}) // Reinitialize map as initialized fails on unmarshalling
}
if len(iter.PageState()) == 0 {
break
}
}
if err := iter.Close(); err != nil {
return nil, serviceerror.NewUnavailablef("GetTaskQueuesByBuildId operation failed. Error: %v", err)
}
return taskQueues, nil
}
func (d *MatchingTaskStore) CountTaskQueuesByBuildId(ctx context.Context, request *p.CountTaskQueuesByBuildIdRequest) (int, error) {
var count int
query := d.Session.Query(templateCountTaskQueueByBuildIdQuery, request.NamespaceID, request.BuildID).WithContext(ctx)
err := query.Scan(&count)
return count, err
}
func (d *MatchingTaskStore) GetName() string {
return cassandraPersistenceName
}
func (d *MatchingTaskStore) Close() {
if d.Session != nil {
d.Session.Close()
}
}