-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtasks.cpp
More file actions
261 lines (237 loc) · 6.27 KB
/
Copy pathtasks.cpp
File metadata and controls
261 lines (237 loc) · 6.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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
#include "app/tasks.hpp"
#include "net/json.hpp"
#include <atomic>
#include <chrono>
#include <sstream>
namespace droidcli::app {
namespace {
int64_t current_timestamp_ms()
{
using namespace std::chrono;
return duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
}
core::String generate_task_id()
{
static std::atomic<int64_t> counter { 0 };
const int64_t sequence = ++counter;
std::ostringstream stream;
stream << "task-" << current_timestamp_ms() << "-" << sequence;
return stream.str();
}
} // namespace
core::String TaskQueue::enqueue(Task task)
{
if (task.id.empty())
{
task.id = generate_task_id();
}
if (task.status.empty())
{
task.status = "pending";
}
const int64_t now = current_timestamp_ms();
task.created_at_ms = now;
task.updated_at_ms = now;
tasks_.push_back(task);
trim_history();
return task.id;
}
std::optional<Task> TaskQueue::claim_next()
{
const int64_t now = current_timestamp_ms();
for (Task& task : tasks_)
{
if (task.status != "pending")
{
continue;
}
if (task.scheduled_for_ms > now)
{
// Not due yet - a scheduled task stays pending (and unclaimed by
// any other pending task behind it) until the wall clock reaches
// its deadline. tick_tasks() calling this every poll iteration
// means it becomes claimable within one iteration of becoming due,
// with no separate scheduler thread needed.
continue;
}
task.status = "running";
task.updated_at_ms = now;
++task.run_count;
return task;
}
return std::nullopt;
}
bool TaskQueue::complete(const core::String& task_id, const core::String& result_json)
{
for (Task& task : tasks_)
{
if (task.id == task_id)
{
task.updated_at_ms = current_timestamp_ms();
task.result_json = result_json;
if (task.recurrence_ms > 0)
{
// Cycle back to pending rather than terminate - see Task's
// status comment. error_message from a prior failed run is
// deliberately left alone here (not cleared) so it stays
// visible as "the last time this failed" even after a
// subsequent successful run, until the task is cancelled.
task.status = "pending";
task.scheduled_for_ms = task.updated_at_ms + task.recurrence_ms;
}
else
{
task.status = "done";
}
trim_history();
return true;
}
}
return false;
}
bool TaskQueue::fail(const core::String& task_id, const core::String& error)
{
for (Task& task : tasks_)
{
if (task.id == task_id)
{
task.error_message = error;
task.updated_at_ms = current_timestamp_ms();
if (task.recurrence_ms > 0)
{
// Keep trying on the next scheduled run instead of dying on
// one failure - matches cron/SOP semantics. The failure is
// still recorded in error_message above.
task.status = "pending";
task.scheduled_for_ms = task.updated_at_ms + task.recurrence_ms;
}
else
{
task.status = "failed";
}
trim_history();
return true;
}
}
return false;
}
bool TaskQueue::cancel(const core::String& task_id)
{
for (Task& task : tasks_)
{
if (task.id == task_id)
{
if (task.status == "done" || task.status == "failed" || task.status == "cancelled")
{
return false;
}
task.status = "cancelled";
task.updated_at_ms = current_timestamp_ms();
trim_history();
return true;
}
}
return false;
}
std::optional<Task> TaskQueue::find(const core::String& task_id) const
{
for (const Task& task : tasks_)
{
if (task.id == task_id)
{
return task;
}
}
return std::nullopt;
}
core::Array<Task> TaskQueue::list() const
{
return tasks_;
}
void TaskQueue::trim_history()
{
// Only trim completed/failed tasks, oldest first, to keep pending/running
// entries around even if that exceeds the cap.
while (tasks_.size() > kMaxHistoryEntries)
{
bool trimmed = false;
for (auto iterator = tasks_.begin(); iterator != tasks_.end(); ++iterator)
{
if (iterator->status == "done" || iterator->status == "failed" || iterator->status == "cancelled")
{
tasks_.erase(iterator);
trimmed = true;
break;
}
}
if (!trimmed)
{
break;
}
}
}
core::String build_task_json(const Task& task)
{
std::ostringstream stream;
stream << '{';
stream << net::json_string_field("id", task.id) << ',';
stream << net::json_string_field("connector_id", task.connector_id) << ',';
stream << net::json_string_field("command", task.command) << ',';
stream << net::json_string_field("payload_json", task.payload_json) << ',';
stream << net::json_string_field("status", task.status) << ',';
stream << "\"created_at_ms\":" << task.created_at_ms << ',';
stream << "\"updated_at_ms\":" << task.updated_at_ms << ',';
stream << "\"scheduled_for_ms\":" << task.scheduled_for_ms << ',';
stream << "\"recurrence_ms\":" << task.recurrence_ms << ',';
stream << "\"run_count\":" << task.run_count << ',';
stream << net::json_string_field("error_message", task.error_message) << ',';
stream << net::json_string_field("result_json", task.result_json);
stream << '}';
return stream.str();
}
core::String build_tasks_json(const core::Array<Task>& tasks)
{
std::ostringstream stream;
// "ok" first - see the identical comment in net::build_connectors_json;
// this is also the list_tasks agent tool's result and needs the same
// field for the same reason.
stream << net::json_bool_field("ok", true) << ",\"tasks\":[";
for (size_t index = 0; index < tasks.size(); ++index)
{
if (index > 0)
{
stream << ',';
}
stream << build_task_json(tasks[index]);
}
stream << "]}";
return stream.str();
}
bool parse_task_request_from_json(
const core::String& json,
Task& out_task,
core::String& out_error)
{
out_task = Task {};
out_error.clear();
out_task.command = net::extract_json_string_field(json, "command");
if (out_task.command.empty())
{
out_error = "Task requires a command.";
return false;
}
out_task.connector_id = net::extract_json_string_field(json, "connector_id");
out_task.payload_json = net::extract_json_string_field(json, "payload_json");
int64_t delay_ms = 0;
if (net::extract_json_int_field(json, "delay_ms", delay_ms) && delay_ms > 0)
{
out_task.scheduled_for_ms = current_timestamp_ms() + delay_ms;
}
int64_t recurrence_ms = 0;
if (net::extract_json_int_field(json, "recurrence_ms", recurrence_ms) && recurrence_ms > 0)
{
out_task.recurrence_ms = recurrence_ms;
}
return true;
}
} // namespace droidcli::app