@voxgig/sdkgen 4.2.6 → 4.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/voxgig-sdkgen +1 -1
- package/dist/helpers/naming.js +5 -4
- package/dist/helpers/naming.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/project/.sdk/model/feature/cost.aon +43 -0
- package/project/.sdk/model/feature/feature-index.aon +1 -0
- package/project/.sdk/src/cmp/js/fragment/EntityCreateOp.fragment.js +2 -1
- package/project/.sdk/src/cmp/js/fragment/EntityListOp.fragment.js +2 -1
- package/project/.sdk/src/cmp/js/fragment/EntityLoadOp.fragment.js +2 -1
- package/project/.sdk/src/cmp/js/fragment/EntityRemoveOp.fragment.js +2 -1
- package/project/.sdk/src/cmp/js/fragment/EntityUpdateOp.fragment.js +2 -1
- package/project/.sdk/src/cmp/py/ReadmeExamplesTest_py.ts +15 -5
- package/project/.sdk/src/cmp/ts/fragment/EntityCreateOp.fragment.ts +2 -1
- package/project/.sdk/src/cmp/ts/fragment/EntityListOp.fragment.ts +2 -1
- package/project/.sdk/src/cmp/ts/fragment/EntityLoadOp.fragment.ts +2 -1
- package/project/.sdk/src/cmp/ts/fragment/EntityRemoveOp.fragment.ts +2 -1
- package/project/.sdk/src/cmp/ts/fragment/EntityUpdateOp.fragment.ts +2 -1
- package/project/.sdk/tm/c/core/sdk.h +1 -0
- package/project/.sdk/tm/c/feature/cost.c +549 -0
- package/project/.sdk/tm/c/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/clojure/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/cpp/feature/cost.hpp +419 -0
- package/project/.sdk/tm/cpp/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/csharp/feature/CostFeature.cs +518 -0
- package/project/.sdk/tm/csharp/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/dart/lib/feature/cost/CostFeature.dart +446 -0
- package/project/.sdk/tm/dart/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/elixir/lib/projectname/feature/cost.ex +442 -0
- package/project/.sdk/tm/elixir/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/go/feature/cost_feature.go +424 -0
- package/project/.sdk/tm/go/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/go-cli/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/go-mcp/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/java/feature/CostFeature.java +431 -0
- package/project/.sdk/tm/java/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/js/src/feature/cost/CostFeature.js +413 -0
- package/project/.sdk/tm/kotlin/feature/CostFeature.kt +430 -0
- package/project/.sdk/tm/kotlin/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/lean/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/lua/feature/cost_feature.lua +457 -0
- package/project/.sdk/tm/lua/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/ocaml/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/perl/feature/cost_feature.pm +446 -0
- package/project/.sdk/tm/perl/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/php/feature/CostFeature.php +439 -0
- package/project/.sdk/tm/php/feature/TestFeature.php +31 -1
- package/project/.sdk/tm/php/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/py/pkg/feature/cost_feature.py +381 -0
- package/project/.sdk/tm/py/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/py-data/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/rb/feature/cost_feature.rb +386 -0
- package/project/.sdk/tm/rb/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/rust/feature/cost.rs +467 -0
- package/project/.sdk/tm/rust/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/scala/feature/CostFeature.scala +388 -0
- package/project/.sdk/tm/scala/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/CostFeature.swift +425 -0
- package/project/.sdk/tm/swift/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/ts/src/feature/cost/CostFeature.ts +415 -0
- package/project/.sdk/tm/zig/feature/cost.zig +457 -0
- package/project/.sdk/tm/zig/src/feature/cost/.gitkeep +0 -0
- package/project/sdkgen-package.json +2 -1
- package/src/helpers/naming.ts +5 -4
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
// Cost tracking and spend budget (mirrors ts
|
|
2
|
+
// src/feature/cost/CostFeature.ts). Uses BOTH seams, which is the point of
|
|
3
|
+
// the feature: money is spent per HTTP ATTEMPT (a retried call is charged
|
|
4
|
+
// again, because the upstream API charges it again), but it is owed by an
|
|
5
|
+
// OPERATION. So the transport wrap prices each attempt, and PreDone
|
|
6
|
+
// attributes the running total to `<entity>.<op>` and to the caller (the
|
|
7
|
+
// per-call ctrl actor, the same actor the audit feature records).
|
|
8
|
+
//
|
|
9
|
+
// The price of an attempt comes from the first source that answers: a
|
|
10
|
+
// response header (`header` x `perUnit`), the rate table (`rates`, keyed
|
|
11
|
+
// `<entity>.<op>` / `<op>` / `*`), then the flat `unit`. A body figure
|
|
12
|
+
// (`path` x `perUnit`, e.g. "usage.total_tokens") is read at PreDone
|
|
13
|
+
// instead, from the already-parsed result, and describes the whole call, so
|
|
14
|
+
// it REPLACES the per-attempt estimate rather than adding to it.
|
|
15
|
+
//
|
|
16
|
+
// `budget` caps total spend. With `onBudget: "deny"` a further operation is
|
|
17
|
+
// refused at PrePoint (via ctx.out point, which make_point surfaces), before
|
|
18
|
+
// an endpoint is resolved and before anything reaches the network.
|
|
19
|
+
//
|
|
20
|
+
// ORDER MATTERS. Cost must sit INSIDE the cache, or a response served from
|
|
21
|
+
// cache is charged for money that was never spent. The default (map) order
|
|
22
|
+
// puts cache innermost and cost outside it, so activate them in list form
|
|
23
|
+
// with cost first.
|
|
24
|
+
|
|
25
|
+
#include "sdk.h"
|
|
26
|
+
|
|
27
|
+
#include <stdio.h>
|
|
28
|
+
#include <stdlib.h>
|
|
29
|
+
#include <string.h>
|
|
30
|
+
|
|
31
|
+
#define COST_PENDING_KEY "cost_pending"
|
|
32
|
+
|
|
33
|
+
typedef struct {
|
|
34
|
+
int64_t calls;
|
|
35
|
+
double amount;
|
|
36
|
+
} CostBucket;
|
|
37
|
+
|
|
38
|
+
// A find-or-insert string->bucket map as parallel growable arrays, the same
|
|
39
|
+
// shape metrics uses for its per-op buckets.
|
|
40
|
+
typedef struct {
|
|
41
|
+
char** keys;
|
|
42
|
+
CostBucket* buckets;
|
|
43
|
+
size_t len;
|
|
44
|
+
size_t cap;
|
|
45
|
+
} CostMap;
|
|
46
|
+
|
|
47
|
+
typedef struct {
|
|
48
|
+
// Aggregates (mirrors the ts client._cost record).
|
|
49
|
+
char* currency;
|
|
50
|
+
int64_t calls;
|
|
51
|
+
int64_t attempts;
|
|
52
|
+
double amount;
|
|
53
|
+
double reported;
|
|
54
|
+
double estimated;
|
|
55
|
+
CostMap ops;
|
|
56
|
+
CostMap actors;
|
|
57
|
+
double limit;
|
|
58
|
+
double spent;
|
|
59
|
+
double remaining;
|
|
60
|
+
bool exceeded;
|
|
61
|
+
voxgig_value* last;
|
|
62
|
+
int64_t seq;
|
|
63
|
+
} CostTrack;
|
|
64
|
+
|
|
65
|
+
typedef struct {
|
|
66
|
+
Feature base;
|
|
67
|
+
char* name;
|
|
68
|
+
bool active;
|
|
69
|
+
voxgig_value* add_opts;
|
|
70
|
+
voxgig_value* options;
|
|
71
|
+
CostTrack* track;
|
|
72
|
+
} CostFeature;
|
|
73
|
+
|
|
74
|
+
typedef struct {
|
|
75
|
+
Fetcher* inner;
|
|
76
|
+
voxgig_value* options;
|
|
77
|
+
CostTrack* track;
|
|
78
|
+
} CostState;
|
|
79
|
+
|
|
80
|
+
static CostBucket* cost_map_entry(CostMap* m, const char* key) {
|
|
81
|
+
for (size_t i = 0; i < m->len; i++) {
|
|
82
|
+
if (strcmp(m->keys[i], key) == 0) return &m->buckets[i];
|
|
83
|
+
}
|
|
84
|
+
if (m->len == m->cap) {
|
|
85
|
+
size_t nc = m->cap ? m->cap * 2 : 8;
|
|
86
|
+
m->keys = (char**)realloc(m->keys, nc * sizeof(char*));
|
|
87
|
+
m->buckets = (CostBucket*)realloc(m->buckets, nc * sizeof(CostBucket));
|
|
88
|
+
m->cap = nc;
|
|
89
|
+
}
|
|
90
|
+
m->keys[m->len] = strdup(key);
|
|
91
|
+
memset(&m->buckets[m->len], 0, sizeof(CostBucket));
|
|
92
|
+
return &m->buckets[m->len++];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
static void cost_bump(CostMap* m, const char* key, double amount) {
|
|
96
|
+
CostBucket* b = cost_map_entry(m, key);
|
|
97
|
+
b->calls += 1;
|
|
98
|
+
b->amount += amount;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
static char* cost_dot_join(const char* a, const char* b) {
|
|
102
|
+
size_t na = a ? strlen(a) : 0;
|
|
103
|
+
size_t nb = b ? strlen(b) : 0;
|
|
104
|
+
char* s = (char*)malloc(na + nb + 2);
|
|
105
|
+
memcpy(s, a ? a : "", na);
|
|
106
|
+
s[na] = '.';
|
|
107
|
+
memcpy(s + na + 1, b ? b : "", nb);
|
|
108
|
+
s[na + 1 + nb] = '\0';
|
|
109
|
+
return s;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// The model records the body figure as a dot path; getpath_c takes a
|
|
113
|
+
// NULL-terminated segment array. Sized from the path itself so a long path is
|
|
114
|
+
// never silently truncated. (paging.c carries its own copy for the same
|
|
115
|
+
// reason: feature source is trimmed per project, so a shared static helper
|
|
116
|
+
// would vanish with whichever feature happened to own it.)
|
|
117
|
+
// Defined below; `through` commits raw (unpipelined) calls through it.
|
|
118
|
+
static void cost_commit(CostTrack* t, voxgig_value* options, Context* ctx,
|
|
119
|
+
voxgig_value* pending, const char* entity, const char* opname);
|
|
120
|
+
|
|
121
|
+
// A fresh per-operation accumulator.
|
|
122
|
+
static voxgig_value* cost_new_pending(void) {
|
|
123
|
+
return cmap(6, "attempts", v_num(0), "amount", v_num(0), "reported", v_num(0),
|
|
124
|
+
"estimated", v_num(0), "source", v_str("none"), "piped", v_num(0));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
static voxgig_value* cost_getpath_dotted(voxgig_value* store, const char* path) {
|
|
128
|
+
if (!path || path[0] == '\0') return store;
|
|
129
|
+
|
|
130
|
+
size_t len = strlen(path);
|
|
131
|
+
size_t maxseg = 2;
|
|
132
|
+
for (size_t i = 0; i < len; i++) {
|
|
133
|
+
if (path[i] == '.') maxseg++;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
char* buf = (char*)malloc(len + 1);
|
|
137
|
+
const char** keys = (const char**)malloc(maxseg * sizeof(char*));
|
|
138
|
+
if (!buf || !keys) {
|
|
139
|
+
free(buf);
|
|
140
|
+
free(keys);
|
|
141
|
+
return voxgig_new_undef();
|
|
142
|
+
}
|
|
143
|
+
memcpy(buf, path, len + 1);
|
|
144
|
+
|
|
145
|
+
size_t n = 0;
|
|
146
|
+
char* seg = buf;
|
|
147
|
+
for (char* p = buf;; p++) {
|
|
148
|
+
if (*p == '.' || *p == '\0') {
|
|
149
|
+
bool end = (*p == '\0');
|
|
150
|
+
*p = '\0';
|
|
151
|
+
if (seg[0] != '\0') keys[n++] = seg;
|
|
152
|
+
seg = p + 1;
|
|
153
|
+
if (end) break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
keys[n] = NULL;
|
|
157
|
+
|
|
158
|
+
voxgig_value* out = getpath_c(store, keys);
|
|
159
|
+
free(buf);
|
|
160
|
+
free(keys);
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
static double cost_per_unit(voxgig_value* options) {
|
|
165
|
+
return fopt_num(options, "perUnit", 0.0);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// The rate table uses the same lookup grammar as rbac's rules:
|
|
169
|
+
// `<entity>.<op>`, then `<op>`, then `*`.
|
|
170
|
+
static bool cost_rate(voxgig_value* options, Context* ctx, double* out) {
|
|
171
|
+
voxgig_value* rates = fopt_map(options, "rates");
|
|
172
|
+
if (!v_is_map(rates)) return false;
|
|
173
|
+
|
|
174
|
+
const char* entity = ctx->op ? ctx->op->entity : "";
|
|
175
|
+
const char* opname = ctx->op ? ctx->op->name : "";
|
|
176
|
+
char* joined = cost_dot_join(entity, opname);
|
|
177
|
+
|
|
178
|
+
const char* keys[3];
|
|
179
|
+
keys[0] = joined;
|
|
180
|
+
keys[1] = opname;
|
|
181
|
+
keys[2] = "*";
|
|
182
|
+
|
|
183
|
+
bool found = false;
|
|
184
|
+
for (int i = 0; i < 3 && !found; i++) {
|
|
185
|
+
voxgig_value* v = getp(rates, keys[i]);
|
|
186
|
+
if (voxgig_is_number(v)) {
|
|
187
|
+
*out = voxgig_as_double(v);
|
|
188
|
+
found = true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
free(joined);
|
|
193
|
+
return found;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Price one attempt: a reported header figure, else the rate table, else the
|
|
197
|
+
// flat unit. `source` receives a static string.
|
|
198
|
+
static double cost_price(voxgig_value* options, Context* ctx, voxgig_value* res,
|
|
199
|
+
const char** source) {
|
|
200
|
+
const char* header = fopt_str(options, "header", "");
|
|
201
|
+
if (header[0] != '\0') {
|
|
202
|
+
const char* raw = fres_header(res, header);
|
|
203
|
+
if (raw && raw[0] != '\0') {
|
|
204
|
+
char* endp = NULL;
|
|
205
|
+
double n = strtod(raw, &endp);
|
|
206
|
+
if (endp != raw) {
|
|
207
|
+
*source = "header";
|
|
208
|
+
return n * cost_per_unit(options);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
double rate = 0.0;
|
|
214
|
+
if (cost_rate(options, ctx, &rate)) {
|
|
215
|
+
*source = "table";
|
|
216
|
+
return rate;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
double unit = fopt_num(options, "unit", 0.0);
|
|
220
|
+
if (unit != 0.0) {
|
|
221
|
+
*source = "unit";
|
|
222
|
+
return unit;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
*source = "none";
|
|
226
|
+
return 0.0;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// A usage figure from the parsed result body, priced by perUnit. Read here,
|
|
230
|
+
// not at the transport seam, because the body is one-shot.
|
|
231
|
+
static bool cost_body_amount(voxgig_value* options, Context* ctx, double* out) {
|
|
232
|
+
const char* path = fopt_str(options, "path", "");
|
|
233
|
+
if (path[0] == '\0') return false;
|
|
234
|
+
if (!ctx->result || !ctx->result->body) return false;
|
|
235
|
+
|
|
236
|
+
voxgig_value* v = cost_getpath_dotted(ctx->result->body, path);
|
|
237
|
+
if (voxgig_is_number(v)) {
|
|
238
|
+
*out = voxgig_as_double(v) * cost_per_unit(options);
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
if (v_is_str(v)) {
|
|
242
|
+
const char* s = voxgig_as_string(v);
|
|
243
|
+
if (s) {
|
|
244
|
+
char* endp = NULL;
|
|
245
|
+
double n = strtod(s, &endp);
|
|
246
|
+
if (endp != s) {
|
|
247
|
+
*out = n * cost_per_unit(options);
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
static void cost_spend(CostTrack* t, double amount, double reported, double estimated) {
|
|
256
|
+
t->amount += amount;
|
|
257
|
+
t->reported += reported;
|
|
258
|
+
t->estimated += estimated;
|
|
259
|
+
|
|
260
|
+
t->spent = t->amount;
|
|
261
|
+
if (t->limit > 0.0) {
|
|
262
|
+
t->remaining = t->limit - t->amount;
|
|
263
|
+
if (t->remaining < 0.0) t->remaining = 0.0;
|
|
264
|
+
if (t->amount >= t->limit) t->exceeded = true;
|
|
265
|
+
} else {
|
|
266
|
+
t->remaining = 0.0;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
static voxgig_value* through(Fetcher* self, Context* ctx, const char* url,
|
|
271
|
+
voxgig_value* fetchdef, PNError** err) {
|
|
272
|
+
CostState* st = (CostState*)self->state;
|
|
273
|
+
voxgig_value* options = st->options;
|
|
274
|
+
CostTrack* track = st->track;
|
|
275
|
+
|
|
276
|
+
voxgig_value* out = st->inner->fn(st->inner, ctx, url, fetchdef, err);
|
|
277
|
+
|
|
278
|
+
const char* source = "none";
|
|
279
|
+
double amount = cost_price(options, ctx, out, &source);
|
|
280
|
+
|
|
281
|
+
// Accumulated on the context, committed once at PreDone. Adding each
|
|
282
|
+
// attempt to the running total and then subtracting it again when a body
|
|
283
|
+
// figure supersedes it loses precision to catastrophic cancellation.
|
|
284
|
+
voxgig_value* pending = ctx_out_extra_get(ctx, COST_PENDING_KEY);
|
|
285
|
+
if (!v_is_map(pending)) {
|
|
286
|
+
pending = cost_new_pending();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
double prev_amount = 0.0;
|
|
290
|
+
int64_t prev_attempts = 0;
|
|
291
|
+
bool piped = false;
|
|
292
|
+
voxgig_value* pa = getp(pending, "amount");
|
|
293
|
+
if (voxgig_is_number(pa)) prev_amount = voxgig_as_double(pa);
|
|
294
|
+
voxgig_value* pn = getp(pending, "attempts");
|
|
295
|
+
if (voxgig_is_number(pn)) prev_attempts = (int64_t)voxgig_as_double(pn);
|
|
296
|
+
voxgig_value* pp = getp(pending, "piped");
|
|
297
|
+
if (voxgig_is_number(pp)) piped = voxgig_as_double(pp) != 0.0;
|
|
298
|
+
|
|
299
|
+
// Reported and estimated are kept apart per ATTEMPT: a 503 priced from the
|
|
300
|
+
// rate table followed by a 200 carrying the cost header is part estimate,
|
|
301
|
+
// part reported, and collapsing both into the final attempt's category
|
|
302
|
+
// would corrupt the split.
|
|
303
|
+
//
|
|
304
|
+
// A failed transport arrives through the err out-param here, not as a
|
|
305
|
+
// longjmp, so it already reaches this point and is priced from the table or
|
|
306
|
+
// unit - no separate catch is needed, unlike the ts/js ports.
|
|
307
|
+
const char* bucket =
|
|
308
|
+
(strcmp(source, "header") == 0 || strcmp(source, "body") == 0) ? "reported" : "estimated";
|
|
309
|
+
double prev_bucket = 0.0;
|
|
310
|
+
voxgig_value* pb = getp(pending, bucket);
|
|
311
|
+
if (voxgig_is_number(pb)) prev_bucket = voxgig_as_double(pb);
|
|
312
|
+
|
|
313
|
+
setp(pending, "attempts", v_num((double)(prev_attempts + 1)));
|
|
314
|
+
setp(pending, "amount", v_num(prev_amount + amount));
|
|
315
|
+
setp(pending, bucket, v_num(prev_bucket + amount));
|
|
316
|
+
setp(pending, "source", v_str(source));
|
|
317
|
+
|
|
318
|
+
track->attempts += 1;
|
|
319
|
+
|
|
320
|
+
// direct() and graphql() reach the transport without dispatching any
|
|
321
|
+
// pipeline hooks, so there is no PrePoint to gate on and no PreDone to
|
|
322
|
+
// commit. Their spend is committed here, or it would never be counted.
|
|
323
|
+
if (!piped) {
|
|
324
|
+
cost_commit(track, options, ctx, pending, "_", "direct");
|
|
325
|
+
ctx_out_extra_set(ctx, COST_PENDING_KEY, v_undef());
|
|
326
|
+
} else {
|
|
327
|
+
ctx_out_extra_set(ctx, COST_PENDING_KEY, pending);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return out;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// The budget gate. Runs before endpoint resolution, so a refused call costs
|
|
334
|
+
// nothing at all.
|
|
335
|
+
static void cost_pre_point(CostFeature* cf, Context* ctx) {
|
|
336
|
+
if (!cf->active) return;
|
|
337
|
+
|
|
338
|
+
// Mark the context as running through the pipeline, so `through` knows a
|
|
339
|
+
// PreDone is coming and does not commit the spend itself.
|
|
340
|
+
voxgig_value* pending = ctx_out_extra_get(ctx, COST_PENDING_KEY);
|
|
341
|
+
if (!v_is_map(pending)) {
|
|
342
|
+
pending = cost_new_pending();
|
|
343
|
+
}
|
|
344
|
+
setp(pending, "piped", v_num(1));
|
|
345
|
+
ctx_out_extra_set(ctx, COST_PENDING_KEY, pending);
|
|
346
|
+
|
|
347
|
+
CostTrack* t = cf->track;
|
|
348
|
+
if (t->limit <= 0.0 || t->amount < t->limit) return;
|
|
349
|
+
|
|
350
|
+
t->exceeded = true;
|
|
351
|
+
|
|
352
|
+
if (strcmp(fopt_str(cf->options, "onBudget", "warn"), "deny") != 0) return;
|
|
353
|
+
|
|
354
|
+
char msg[320];
|
|
355
|
+
snprintf(msg, sizeof(msg), "Cost budget of %g %s is spent (%g %s used)", t->limit,
|
|
356
|
+
t->currency, t->amount, t->currency);
|
|
357
|
+
PNError* err = context_make_error(ctx, "cost_budget", msg);
|
|
358
|
+
|
|
359
|
+
// Short-circuit endpoint resolution; make_point surfaces this error before
|
|
360
|
+
// any network activity.
|
|
361
|
+
ctx_out_set_point_err(ctx, err);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Commit one operation's spend: totals, budget, per-op and per-actor
|
|
365
|
+
// attribution, and the record. Shared by cost_finish and the raw-request path
|
|
366
|
+
// in through, which has no PreDone to reach.
|
|
367
|
+
static void cost_commit(CostTrack* t, voxgig_value* options, Context* ctx,
|
|
368
|
+
voxgig_value* pending, const char* entity, const char* opname) {
|
|
369
|
+
double amount = 0.0;
|
|
370
|
+
double reported = 0.0;
|
|
371
|
+
double estimated = 0.0;
|
|
372
|
+
int64_t attempts = 0;
|
|
373
|
+
const char* source = "none";
|
|
374
|
+
|
|
375
|
+
voxgig_value* pa = getp(pending, "amount");
|
|
376
|
+
if (voxgig_is_number(pa)) amount = voxgig_as_double(pa);
|
|
377
|
+
voxgig_value* pr = getp(pending, "reported");
|
|
378
|
+
if (voxgig_is_number(pr)) reported = voxgig_as_double(pr);
|
|
379
|
+
voxgig_value* pe = getp(pending, "estimated");
|
|
380
|
+
if (voxgig_is_number(pe)) estimated = voxgig_as_double(pe);
|
|
381
|
+
voxgig_value* pn = getp(pending, "attempts");
|
|
382
|
+
if (voxgig_is_number(pn)) attempts = (int64_t)voxgig_as_double(pn);
|
|
383
|
+
voxgig_value* ps = getp(pending, "source");
|
|
384
|
+
if (v_is_str(ps)) {
|
|
385
|
+
const char* sv = voxgig_as_string(ps);
|
|
386
|
+
if (sv) source = sv;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// A body figure prices the whole call, so it replaces the per-attempt
|
|
390
|
+
// estimate rather than adding to it, and being server-stated the whole
|
|
391
|
+
// amount counts as reported.
|
|
392
|
+
double body = 0.0;
|
|
393
|
+
if (cost_body_amount(options, ctx, &body)) {
|
|
394
|
+
amount = body;
|
|
395
|
+
reported = body;
|
|
396
|
+
estimated = 0.0;
|
|
397
|
+
source = "body";
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
cost_spend(t, amount, reported, estimated);
|
|
401
|
+
|
|
402
|
+
const char* actor = "anonymous";
|
|
403
|
+
const char* opt_actor = fopt_str(options, "actor", "");
|
|
404
|
+
if (opt_actor[0] != '\0') actor = opt_actor;
|
|
405
|
+
if (ctx->ctrl && ctx->ctrl->actor && ctx->ctrl->actor[0] != '\0') actor = ctx->ctrl->actor;
|
|
406
|
+
|
|
407
|
+
t->calls += 1;
|
|
408
|
+
char* key = cost_dot_join(entity, opname);
|
|
409
|
+
cost_bump(&t->ops, key, amount);
|
|
410
|
+
free(key);
|
|
411
|
+
cost_bump(&t->actors, actor, amount);
|
|
412
|
+
|
|
413
|
+
t->seq += 1;
|
|
414
|
+
voxgig_value* record =
|
|
415
|
+
cmap(8, "seq", v_num((double)t->seq), "entity", v_str(entity), "op", v_str(opname),
|
|
416
|
+
"actor", v_str(actor), "amount", v_num(amount), "currency", v_str(t->currency),
|
|
417
|
+
"source", v_str(source), "attempts", v_num((double)attempts));
|
|
418
|
+
t->last = record;
|
|
419
|
+
|
|
420
|
+
voxgig_value* sink = getp(options, "sink");
|
|
421
|
+
if (voxgig_is_func(sink)) {
|
|
422
|
+
call_vfn(sink, record);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Attribute the operation's spend once the call is finished. PreDone and
|
|
427
|
+
// PreUnexpected share this: a FAILED operation still spent the money, and when
|
|
428
|
+
// the pipeline errors PreDone never runs. Whichever hook fires first consumes
|
|
429
|
+
// the pending entry, so it commits exactly once.
|
|
430
|
+
static void cost_finish(CostFeature* cf, Context* ctx, bool done) {
|
|
431
|
+
if (!cf->active) return;
|
|
432
|
+
|
|
433
|
+
voxgig_value* pending = ctx_out_extra_get(ctx, COST_PENDING_KEY);
|
|
434
|
+
if (!v_is_map(pending)) return;
|
|
435
|
+
ctx_out_extra_set(ctx, COST_PENDING_KEY, v_undef());
|
|
436
|
+
|
|
437
|
+
// A FAILED operation that made no attempt never reached the network:
|
|
438
|
+
// PrePoint creates the pending entry to mark the context as piped, and
|
|
439
|
+
// then the budget gate refuses the call (rbac, or an unresolvable
|
|
440
|
+
// endpoint, short-circuits just as early). Committing it would count a
|
|
441
|
+
// call that never happened and file a zero-amount record as `last`.
|
|
442
|
+
//
|
|
443
|
+
// A SUCCEEDED operation that made no attempt is the opposite case: it was
|
|
444
|
+
// served from the cache. That is a real call, and the fact that it cost
|
|
445
|
+
// nothing is the whole point of ordering cost inside the cache.
|
|
446
|
+
if (!done) {
|
|
447
|
+
voxgig_value* an = getp(pending, "attempts");
|
|
448
|
+
if (!voxgig_is_number(an) || 0 == (int64_t)voxgig_as_double(an)) return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const char* entity = (ctx->op && ctx->op->entity[0] != '\0') ? ctx->op->entity : "_";
|
|
452
|
+
const char* opname = (ctx->op && ctx->op->name[0] != '\0') ? ctx->op->name : "_";
|
|
453
|
+
|
|
454
|
+
cost_commit(cf->track, cf->options, ctx, pending, entity, opname);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
static const char* cost_name(Feature* f) { return ((CostFeature*)f)->name; }
|
|
458
|
+
static bool cost_active(Feature* f) { return ((CostFeature*)f)->active; }
|
|
459
|
+
static voxgig_value* cost_add_options(Feature* f) { return ((CostFeature*)f)->add_opts; }
|
|
460
|
+
|
|
461
|
+
static void cost_init(Feature* f, Context* ctx, voxgig_value* options) {
|
|
462
|
+
CostFeature* cf = (CostFeature*)f;
|
|
463
|
+
cf->options = options;
|
|
464
|
+
cf->active = fopt_bool(options, "active", false);
|
|
465
|
+
|
|
466
|
+
CostTrack* t = cf->track;
|
|
467
|
+
double limit = fopt_num(options, "budget", 0.0);
|
|
468
|
+
|
|
469
|
+
free(t->currency);
|
|
470
|
+
t->currency = strdup(fopt_str(options, "currency", "USD"));
|
|
471
|
+
t->calls = 0;
|
|
472
|
+
t->attempts = 0;
|
|
473
|
+
t->amount = 0.0;
|
|
474
|
+
t->reported = 0.0;
|
|
475
|
+
t->estimated = 0.0;
|
|
476
|
+
t->ops.len = 0;
|
|
477
|
+
t->actors.len = 0;
|
|
478
|
+
t->limit = limit;
|
|
479
|
+
t->spent = 0.0;
|
|
480
|
+
t->remaining = limit;
|
|
481
|
+
t->exceeded = false;
|
|
482
|
+
t->last = voxgig_new_undef();
|
|
483
|
+
t->seq = 0;
|
|
484
|
+
|
|
485
|
+
if (!cf->active) return;
|
|
486
|
+
|
|
487
|
+
Utility* util = context_util(ctx);
|
|
488
|
+
CostState* st = (CostState*)calloc(1, sizeof(CostState));
|
|
489
|
+
st->inner = util->fetcher;
|
|
490
|
+
st->options = options;
|
|
491
|
+
st->track = t;
|
|
492
|
+
|
|
493
|
+
Fetcher* wrapped = (Fetcher*)calloc(1, sizeof(Fetcher));
|
|
494
|
+
wrapped->fn = through;
|
|
495
|
+
wrapped->state = st;
|
|
496
|
+
util->fetcher = wrapped;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
static void cost_hook(Feature* f, const char* name, Context* ctx) {
|
|
500
|
+
CostFeature* cf = (CostFeature*)f;
|
|
501
|
+
if (strcmp(name, "PrePoint") == 0) {
|
|
502
|
+
cost_pre_point(cf, ctx);
|
|
503
|
+
} else if (strcmp(name, "PreDone") == 0) {
|
|
504
|
+
cost_finish(cf, ctx, true);
|
|
505
|
+
} else if (strcmp(name, "PreUnexpected") == 0) {
|
|
506
|
+
cost_finish(cf, ctx, false);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
static voxgig_value* cost_map_value(const CostMap* m) {
|
|
511
|
+
voxgig_value* out = v_map();
|
|
512
|
+
for (size_t i = 0; i < m->len; i++) {
|
|
513
|
+
setp(out, m->keys[i],
|
|
514
|
+
cmap(2, "calls", v_num((double)m->buckets[i].calls), "amount",
|
|
515
|
+
v_num(m->buckets[i].amount)));
|
|
516
|
+
}
|
|
517
|
+
return out;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
static voxgig_value* cost_track(Feature* f) {
|
|
521
|
+
CostFeature* cf = (CostFeature*)f;
|
|
522
|
+
CostTrack* t = cf->track;
|
|
523
|
+
return cmap(
|
|
524
|
+
6, "currency", v_str(t->currency), "total",
|
|
525
|
+
cmap(5, "calls", v_num((double)t->calls), "attempts", v_num((double)t->attempts),
|
|
526
|
+
"amount", v_num(t->amount), "reported", v_num(t->reported), "estimated",
|
|
527
|
+
v_num(t->estimated)),
|
|
528
|
+
"ops", cost_map_value(&t->ops), "actors", cost_map_value(&t->actors), "budget",
|
|
529
|
+
cmap(4, "limit", v_num(t->limit), "spent", v_num(t->spent), "remaining",
|
|
530
|
+
v_num(t->remaining), "exceeded", t->exceeded ? v_num(1) : v_num(0)),
|
|
531
|
+
"last", t->last);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
static const FeatureVT COST_VT = {
|
|
535
|
+
cost_name, cost_active, cost_add_options, cost_init, cost_hook, cost_track,
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
Feature* feature_cost_new(void) {
|
|
539
|
+
CostFeature* cf = (CostFeature*)calloc(1, sizeof(CostFeature));
|
|
540
|
+
cf->base.vt = &COST_VT;
|
|
541
|
+
cf->name = strdup("cost");
|
|
542
|
+
cf->active = true;
|
|
543
|
+
cf->add_opts = NULL;
|
|
544
|
+
cf->options = voxgig_new_undef();
|
|
545
|
+
cf->track = (CostTrack*)calloc(1, sizeof(CostTrack));
|
|
546
|
+
cf->track->currency = strdup("USD");
|
|
547
|
+
cf->track->last = voxgig_new_undef();
|
|
548
|
+
return (Feature*)cf;
|
|
549
|
+
}
|
|
File without changes
|
|
File without changes
|