From 75813275aa281332144b08f50478b221e0636140 Mon Sep 17 00:00:00 2001 From: Jason Lynch Date: Fri, 19 Dec 2025 10:31:31 -0500 Subject: [PATCH] fix: task log pagination Fixes a bug where the `last_entry_id` is empty when `after_entry_id` is the last entry in the log. Instead of performing an exclusive get in Etcd, we perform an inclusive get and then filter out the entry with `entry_id == after_entry_id` in the service layer. To test this change: ```sh # perform an async operation, such as creating a db # note that the follow-task helper should continue to work as expected cp1-req create-database < ./.scratch/requests/db.json | cp-follow-task # once the task is complete, list tasks cp1-req list-database-tasks # get the task logs cp1-req get-database-task-log # take the last entry id from the previous response and use it as the # after-entry-id argument cp1-req get-database-task-log --after-entry-id # note that the entries list is empty, but the last_entry_id is still # populated. # if needed, you can further validate that the correct entries and number of # entries is returned by comparing with the contents in etcd: cp-etcdctl get --sort-by key --prefix /task_log_messages// --keys-only | sort -u | tail -n +2 ``` PLAT-367 --- changes/unreleased/Fixed-20251219-104159.yaml | 3 +++ server/internal/task/service.go | 8 ++++++++ server/internal/task/task_log_store.go | 5 ++++- 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 changes/unreleased/Fixed-20251219-104159.yaml diff --git a/changes/unreleased/Fixed-20251219-104159.yaml b/changes/unreleased/Fixed-20251219-104159.yaml new file mode 100644 index 00000000..8f4be86d --- /dev/null +++ b/changes/unreleased/Fixed-20251219-104159.yaml @@ -0,0 +1,3 @@ +kind: Fixed +body: Fixed task logs API to return a `last_entry_id` even if `after_entry_id` is the last entry. +time: 2025-12-19T10:41:59.153319-05:00 diff --git a/server/internal/task/service.go b/server/internal/task/service.go index da67bdaa..fe41d107 100644 --- a/server/internal/task/service.go +++ b/server/internal/task/service.go @@ -140,9 +140,17 @@ func (s *Service) GetTaskLog(ctx context.Context, databaseID string, taskID uuid log := &TaskLog{ DatabaseID: databaseID, TaskID: taskID, + Entries: make([]LogEntry, 0, len(stored)), } for i := len(stored) - 1; i >= 0; i-- { s := stored[i] + if s.EntryID == options.AfterEntryID { + // This range should be behave as if its exclusive, however we need + // to perform an inclusive get so that we're still able to return + // the last entry ID when there are no entries after AfterEntryID. + // Skipping this entry produces the expected behavior. + continue + } log.Entries = append(log.Entries, LogEntry{ Timestamp: s.Timestamp, Message: s.Message, diff --git a/server/internal/task/task_log_store.go b/server/internal/task/task_log_store.go index 1268aceb..bf0c423e 100644 --- a/server/internal/task/task_log_store.go +++ b/server/internal/task/task_log_store.go @@ -61,7 +61,10 @@ func (s *TaskLogEntryStore) GetAllByTaskID(databaseID string, taskID uuid.UUID, opOptions = append(opOptions, clientv3.WithLimit(int64(options.Limit))) } if options.AfterEntryID != uuid.Nil { - rangeStart = s.Key(databaseID, taskID, options.AfterEntryID) + "0" + // We intentionally treat this as inclusive so that we still return an + // entry when AfterEntryID is the last entry. Callers must ignore the + // entry with EntryID == AfterEntryID. + rangeStart = s.Key(databaseID, taskID, options.AfterEntryID) } opOptions = append( opOptions,