Host C Specs


Directory: src/c/
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 100.0% 56 / 0 / 56
Functions: 100.0% 9 / 0 / 9
Branches: 91.2% 31 / 0 / 34

pebble/io/outbox_queue.h
Line Branch Exec Source
1 /**
2 * @file outbox_queue.h
3 * @brief The outbox's work queue, its failed set, and the slot holding whatever is in flight.
4 *
5 * One AppMessage goes out at a time, so everything the watch wants to send queues here first. A
6 * job the phone nacks is held in the failed set and moved back for a later pass, because its pkjs
7 * was usually asleep or busy and that first send is often what wakes it. Holding it means a whole
8 * poll, or the one settings reply a launch gets, is not lost to one nack.
9 *
10 * None of that needs the SDK. The transport owns one of these, does the sending and the timers, and
11 * asks here what to send next, which keeps the deciding testable on the host.
12 *
13 * @ingroup lib_io
14 */
15 #pragma once
16 #include <stdbool.h>
17
18 /**
19 * @addtogroup lib_io
20 * @{
21 */
22
23 /// Most jobs the work queue or the failed set holds at once
24 #define OUTBOX_QUEUE_MAX 6
25
26 /**
27 * @brief What an outbox job sends.
28 */
29 typedef enum
30 {
31 OUTBOX_NONE, ///< The zero value, so a default-initialized job slot reads as nothing rather than a real request
32 OUTBOX_WEATHER, ///< A weather request
33 OUTBOX_SETTINGS, ///< The settings reply
34 OUTBOX_FRESH, ///< The short settings reply, only whether the watch booted with nothing saved
35 OUTBOX_STOCK, ///< A stock request
36 OUTBOX_CALENDAR ///< A calendar request
37 } OutboxKind;
38
39 /**
40 * @brief One job waiting to go out, or already in flight.
41 */
42 typedef struct
43 {
44 OutboxKind kind; ///< What this job sends
45 int retries_left; ///< Retry passes left after a failed send
46 } OutboxJob;
47
48 /**
49 * @brief Everything the outbox is holding: what is waiting, what nacked, and what is out.
50 *
51 * Zero-initialized is the empty state, since `OUTBOX_NONE` is the enum's zero.
52 */
53 typedef struct
54 {
55 OutboxJob queue[OUTBOX_QUEUE_MAX]; ///< This pass's work queue, oldest first
56 int queue_len; ///< How many jobs are in the queue
57 OutboxJob failed[OUTBOX_QUEUE_MAX]; ///< Requests that nacked this pass, held for the next
58 int failed_len; ///< How many jobs are in the failed set
59 OutboxJob inflight; ///< The job in flight, OUTBOX_NONE when nothing is out
60 } OutboxQueue;
61
62 /**
63 * @brief Whether anything is in flight, which is what says the outbox is busy.
64 *
65 * @param outbox The outbox to read.
66 * @return True while a send is waiting on its sent or failed callback.
67 */
68 6 static inline bool outbox_busy(const OutboxQueue *outbox)
69 {
70 6 return outbox->inflight.kind != OUTBOX_NONE;
71 }
72
73 /**
74 * @brief Whether this kind is already somewhere in the outbox.
75 *
76 * Covers all three places a job can be, so a poll that repeats while an earlier one is still
77 * working its way out does not queue a second copy of the same request.
78 *
79 * @param outbox The outbox to read.
80 * @param kind The job kind to look for.
81 * @return True when that kind is in flight, queued, or held after a nack.
82 */
83 17 static inline bool outbox_pending(const OutboxQueue *outbox, OutboxKind kind)
84 {
85
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 16 times.
17 if (kind == OUTBOX_NONE)
86 {
87 // the empty flight slot and every unused array entry read as OUTBOX_NONE, so without this
88 // an idle outbox would answer that nothing is already pending
89 1 return false;
90 }
91
92
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 15 times.
16 if (outbox->inflight.kind == kind)
93 {
94 1 return true;
95 }
96
97
2/2
✓ Branch 0 taken 11 times.
✓ Branch 1 taken 14 times.
25 for (int i = 0; i < outbox->queue_len; i++)
98 {
99
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 10 times.
11 if (outbox->queue[i].kind == kind)
100 {
101 1 return true;
102 }
103 }
104
105
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 13 times.
14 for (int i = 0; i < outbox->failed_len; i++)
106 {
107
1/2
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
1 if (outbox->failed[i].kind == kind)
108 {
109 1 return true;
110 }
111 }
112
113 13 return false;
114 }
115
116 /**
117 * @brief Add a job to the work queue unless its kind is already pending.
118 *
119 * @param outbox The outbox to add to.
120 * @param kind The job kind to send.
121 * @param retries Retry passes left if the first send nacks.
122 * @return True when the job was added, false when it was already pending or the queue is full.
123 */
124 16 static inline bool outbox_push(OutboxQueue *outbox, OutboxKind kind, int retries)
125 {
126
4/4
✓ Branch 1 taken 13 times.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 1 time.
✓ Branch 4 taken 12 times.
16 if (outbox_pending(outbox, kind) || outbox->queue_len >= OUTBOX_QUEUE_MAX)
127 {
128 4 return false;
129 }
130
131 12 outbox->queue[outbox->queue_len].kind = kind;
132 12 outbox->queue[outbox->queue_len].retries_left = retries;
133 12 outbox->queue_len++;
134 12 return true;
135 }
136
137 /**
138 * @brief Drop the front job off the work queue, shifting the rest forward.
139 *
140 * @param outbox The outbox to drop from.
141 */
142 6 static inline void outbox_pop_front(OutboxQueue *outbox)
143 {
144
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 6 times.
8 for (int i = 1; i < outbox->queue_len; i++)
145 {
146 2 outbox->queue[i - 1] = outbox->queue[i];
147 }
148
149
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 1 time.
6 if (outbox->queue_len > 0)
150 {
151 5 outbox->queue_len--;
152 }
153 6 }
154
155 /**
156 * @brief Take the head of the queue as the job now in flight.
157 *
158 * The caller sends it first and only takes it once the send was accepted, so a refused send leaves
159 * the head where it was.
160 *
161 * @param outbox The outbox to take from.
162 */
163 5 static inline void outbox_take_head(OutboxQueue *outbox)
164 {
165
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 4 times.
5 if (outbox->queue_len == 0)
166 {
167 1 return; // outbox_clear leaves the array behind, so an empty queue still has one to read
168 }
169
170 4 outbox->inflight = outbox->queue[0];
171 4 outbox_pop_front(outbox);
172 }
173
174 /**
175 * @brief Mark whatever was in flight as settled, whichever way it went.
176 *
177 * @param outbox The outbox to clear.
178 * @return The job that was in flight, so a nacked one can be handed to outbox_hold_failed.
179 */
180 1 static inline OutboxJob outbox_release(OutboxQueue *outbox)
181 {
182 1 OutboxJob settled = outbox->inflight;
183 1 outbox->inflight.kind = OUTBOX_NONE;
184 1 return settled;
185 }
186
187 /**
188 * @brief Hold a nacked job for the next pass, if it still has retries.
189 *
190 * @param outbox The outbox to hold it in.
191 * @param job The job that just nacked.
192 */
193 13 static inline void outbox_hold_failed(OutboxQueue *outbox, OutboxJob job)
194 {
195 // a failure reported once the slot is already free comes with no job, and holding it would
196 // queue a send for nothing ahead of the real requests
197
4/4
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 10 times.
13 if (job.kind == OUTBOX_NONE || job.retries_left <= 0)
198 {
199 3 return;
200 }
201
202
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 if (outbox->failed_len < OUTBOX_QUEUE_MAX)
203 {
204 10 outbox->failed[outbox->failed_len].kind = job.kind;
205 10 outbox->failed[outbox->failed_len].retries_left = job.retries_left - 1;
206 10 outbox->failed_len++;
207 }
208 }
209
210 /**
211 * @brief Move the whole failed set back into the work queue for another pass.
212 *
213 * @param outbox The outbox to move within.
214 * @return How many jobs were moved back.
215 */
216 1 static inline int outbox_retry_pass(OutboxQueue *outbox)
217 {
218 1 int moved = 0;
219
220
3/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
3 while (outbox->failed_len > 0 && outbox->queue_len < OUTBOX_QUEUE_MAX)
221 {
222 2 outbox->queue[outbox->queue_len++] = outbox->failed[0];
223
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 2 times.
3 for (int i = 1; i < outbox->failed_len; i++)
224 {
225 1 outbox->failed[i - 1] = outbox->failed[i];
226 }
227 2 outbox->failed_len--;
228 2 moved++;
229 }
230
231 1 return moved;
232 }
233
234 /**
235 * @brief Forget everything waiting, for a phone that is no longer there.
236 *
237 * Nothing reaches a phone that is not connected, so a store's next poll asks again once it is back
238 * rather than a stale queue draining into it.
239 *
240 * @param outbox The outbox to empty.
241 */
242 1 static inline void outbox_clear(OutboxQueue *outbox)
243 {
244 1 outbox->queue_len = 0;
245 1 outbox->failed_len = 0;
246 1 }
247
248 /** @} */
249