-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathqueue.go
More file actions
160 lines (147 loc) · 4.27 KB
/
queue.go
File metadata and controls
160 lines (147 loc) · 4.27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
package cliutil
import (
"sync"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/codersdk"
)
// Queue is a FIFO queue with a fixed size. If the size is exceeded, the first
// item is dropped.
type Queue[T any] struct {
cond *sync.Cond
items []T
mu sync.Mutex
size int
closed bool
pred func(x T) (T, bool)
}
// NewQueue creates a queue with the given size.
func NewQueue[T any](size int) *Queue[T] {
q := &Queue[T]{
items: make([]T, 0, size),
size: size,
}
q.cond = sync.NewCond(&q.mu)
return q
}
// WithPredicate adds the given predicate function, which can control what is
// pushed to the queue.
func (q *Queue[T]) WithPredicate(pred func(x T) (T, bool)) *Queue[T] {
q.pred = pred
return q
}
// Close aborts any pending pops and makes future pushes error.
func (q *Queue[T]) Close() {
q.mu.Lock()
defer q.mu.Unlock()
q.closed = true
q.cond.Broadcast()
}
// Push adds an item to the queue. If closed, returns an error.
func (q *Queue[T]) Push(x T) error {
q.mu.Lock()
defer q.mu.Unlock()
if q.closed {
return xerrors.New("queue has been closed")
}
// Potentially mutate or skip the push using the predicate.
if q.pred != nil {
var ok bool
x, ok = q.pred(x)
if !ok {
return nil
}
}
// Remove the first item from the queue if it has gotten too big.
if len(q.items) >= q.size {
q.items = q.items[1:]
}
q.items = append(q.items, x)
q.cond.Broadcast()
return nil
}
// Pop removes and returns the first item from the queue, waiting until there is
// something to pop if necessary. If closed, returns false.
func (q *Queue[T]) Pop() (T, bool) {
var head T
q.mu.Lock()
defer q.mu.Unlock()
for len(q.items) == 0 && !q.closed {
q.cond.Wait()
}
if q.closed {
return head, false
}
head, q.items = q.items[0], q.items[1:]
return head, true
}
func (q *Queue[T]) Len() int {
q.mu.Lock()
defer q.mu.Unlock()
return len(q.items)
}
type reportTask struct {
link string
messageID int64
selfReported bool
state codersdk.WorkspaceAppStatusState
summary string
}
// statusQueue is a Queue that:
// 1. Only pushes items that are not duplicates.
// 2. Preserves the existing message and URI when one a message is not provided.
// 3. Ignores "working" updates from the status watcher.
type StatusQueue struct {
Queue[reportTask]
// lastMessageID is the ID of the last *user* message that we saw. A user
// message only happens when interacting via the API (as opposed to
// interacting with the terminal directly).
lastMessageID int64
}
func (q *StatusQueue) Push(report reportTask) error {
q.mu.Lock()
defer q.mu.Unlock()
if q.closed {
return xerrors.New("queue has been closed")
}
var lastReport reportTask
if len(q.items) > 0 {
lastReport = q.items[len(q.items)-1]
}
// Use "working" status if this is a new user message. If this is not a new
// user message, and the status is "working" and not self-reported (meaning it
// came from the screen watcher), then it means one of two things:
// 1. The LLM is still working, in which case our last status will already
// have been "working", so there is nothing to do.
// 2. The user has interacted with the terminal directly. For now, we are
// ignoring these updates. This risks missing cases where the user
// manually submits a new prompt and the LLM becomes active and does not
// update itself, but it avoids spamming useless status updates as the user
// is typing, so the tradeoff is worth it. In the future, if we can
// reliably distinguish between user and LLM activity, we can change this.
if report.messageID > q.lastMessageID {
report.state = codersdk.WorkspaceAppStatusStateWorking
} else if report.state == codersdk.WorkspaceAppStatusStateWorking && !report.selfReported {
q.mu.Unlock()
return nil
}
// Preserve previous message and URI if there was no message.
if report.summary == "" {
report.summary = lastReport.summary
if report.link == "" {
report.link = lastReport.link
}
}
// Avoid queueing duplicate updates.
if report.state == lastReport.state &&
report.link == lastReport.link &&
report.summary == lastReport.summary {
return nil
}
// Drop the first item if the queue has gotten too big.
if len(q.items) >= q.size {
q.items = q.items[1:]
}
q.items = append(q.items, report)
q.cond.Broadcast()
return nil
}