@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,467 @@
|
|
|
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 pre_done
|
|
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 pre_done
|
|
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 pre_point (via ctx.out["point"], which MakePoint surfaces),
|
|
18
|
+
// before 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
|
+
use std::cell::RefCell;
|
|
26
|
+
use std::collections::HashMap;
|
|
27
|
+
use std::rc::Rc;
|
|
28
|
+
|
|
29
|
+
use crate::core::context::Context;
|
|
30
|
+
use crate::core::error::ProjectNameError;
|
|
31
|
+
use crate::core::helpers::{call_vfn, get_f64, getp, getpath, jo, setp};
|
|
32
|
+
use crate::core::types::{Feature, FetcherFn, OutVal};
|
|
33
|
+
use crate::feature::support::*;
|
|
34
|
+
use crate::utility::voxgigstruct::Value;
|
|
35
|
+
|
|
36
|
+
const COST_PENDING_KEY: &str = "cost_pending";
|
|
37
|
+
|
|
38
|
+
#[derive(Default, Clone)]
|
|
39
|
+
pub struct CostBucket {
|
|
40
|
+
pub calls: i64,
|
|
41
|
+
pub amount: f64,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#[derive(Default)]
|
|
45
|
+
pub struct CostTrack {
|
|
46
|
+
// Aggregates (mirrors the ts client._cost record).
|
|
47
|
+
pub currency: String,
|
|
48
|
+
pub calls: i64,
|
|
49
|
+
pub attempts: i64,
|
|
50
|
+
pub amount: f64,
|
|
51
|
+
pub reported: f64,
|
|
52
|
+
pub estimated: f64,
|
|
53
|
+
pub ops: HashMap<String, CostBucket>,
|
|
54
|
+
pub actors: HashMap<String, CostBucket>,
|
|
55
|
+
pub limit: f64,
|
|
56
|
+
pub spent: f64,
|
|
57
|
+
pub remaining: f64,
|
|
58
|
+
pub exceeded: bool,
|
|
59
|
+
pub last: Value,
|
|
60
|
+
pub seq: i64,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
pub struct CostFeature {
|
|
64
|
+
pub name: String,
|
|
65
|
+
pub active: bool,
|
|
66
|
+
pub add_opts: Option<Value>,
|
|
67
|
+
options: Value,
|
|
68
|
+
pub track: Rc<RefCell<CostTrack>>,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
impl CostFeature {
|
|
72
|
+
pub fn new() -> CostFeature {
|
|
73
|
+
CostFeature {
|
|
74
|
+
name: "cost".to_string(),
|
|
75
|
+
active: true,
|
|
76
|
+
add_opts: None,
|
|
77
|
+
options: Value::Noval,
|
|
78
|
+
track: Rc::new(RefCell::new(CostTrack::default())),
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Price one attempt: a reported header figure, else the rate table, else the
|
|
84
|
+
// flat unit.
|
|
85
|
+
fn price(options: &Value, ctx: &Rc<Context>, res: &Value) -> (f64, String) {
|
|
86
|
+
let header = fopt_str(options, "header", "");
|
|
87
|
+
if !header.is_empty() {
|
|
88
|
+
if let Some(s) = fres_header(res, &header) {
|
|
89
|
+
if let Ok(n) = s.trim().parse::<f64>() {
|
|
90
|
+
return (n * fopt_num(options, "perUnit", 0.0), "header".to_string());
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if let Some(rate) = rate(options, ctx) {
|
|
96
|
+
return (rate, "table".to_string());
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let unit = fopt_num(options, "unit", 0.0);
|
|
100
|
+
if unit != 0.0 {
|
|
101
|
+
return (unit, "unit".to_string());
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
(0.0, "none".to_string())
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// The rate table uses the same lookup grammar as rbac's rules:
|
|
108
|
+
// `<entity>.<op>`, then `<op>`, then `*`.
|
|
109
|
+
fn rate(options: &Value, ctx: &Rc<Context>) -> Option<f64> {
|
|
110
|
+
let rates = fopt_map(options, "rates");
|
|
111
|
+
if !matches!(rates, Value::Map(_)) {
|
|
112
|
+
return None;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let (entity, opname) = {
|
|
116
|
+
let op = ctx.op.borrow();
|
|
117
|
+
(op.entity.clone(), op.name.clone())
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
for key in [format!("{}.{}", entity, opname), opname, "*".to_string()] {
|
|
121
|
+
if let Some(n) = get_f64(&rates, &key) {
|
|
122
|
+
return Some(n);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
None
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// A usage figure from the parsed result body, priced by perUnit. Read here,
|
|
129
|
+
// not at the transport seam, because the body is one-shot.
|
|
130
|
+
fn body_amount(options: &Value, ctx: &Rc<Context>) -> Option<f64> {
|
|
131
|
+
let path = fopt_str(options, "path", "");
|
|
132
|
+
if path.is_empty() {
|
|
133
|
+
return None;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let body = match ctx.result.borrow().clone() {
|
|
137
|
+
Some(r) => r.borrow().body.clone(),
|
|
138
|
+
None => return None,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
let segs: Vec<&str> = path.split('.').collect();
|
|
142
|
+
let val = getpath(&segs, &body);
|
|
143
|
+
|
|
144
|
+
let n = match &val {
|
|
145
|
+
Value::Num(n) => *n,
|
|
146
|
+
Value::Str(s) => match s.trim().parse::<f64>() {
|
|
147
|
+
Ok(v) => v,
|
|
148
|
+
Err(_) => return None,
|
|
149
|
+
},
|
|
150
|
+
_ => return None,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
Some(n * fopt_num(options, "perUnit", 0.0))
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
fn spend(t: &mut CostTrack, amount: f64, reported: f64, estimated: f64) {
|
|
157
|
+
t.amount += amount;
|
|
158
|
+
t.reported += reported;
|
|
159
|
+
t.estimated += estimated;
|
|
160
|
+
|
|
161
|
+
t.spent = t.amount;
|
|
162
|
+
if t.limit > 0.0 {
|
|
163
|
+
t.remaining = (t.limit - t.amount).max(0.0);
|
|
164
|
+
if t.amount >= t.limit {
|
|
165
|
+
t.exceeded = true;
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
t.remaining = 0.0;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
fn bump(bucket: &mut HashMap<String, CostBucket>, key: String, amount: f64) {
|
|
173
|
+
let entry = bucket.entry(key).or_default();
|
|
174
|
+
entry.calls += 1;
|
|
175
|
+
entry.amount += amount;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
fn charge(
|
|
179
|
+
track: &Rc<RefCell<CostTrack>>,
|
|
180
|
+
options: &Value,
|
|
181
|
+
ctx: &Rc<Context>,
|
|
182
|
+
url: &str,
|
|
183
|
+
fetchdef: &Value,
|
|
184
|
+
inner: &FetcherFn,
|
|
185
|
+
) -> Result<Value, ProjectNameError> {
|
|
186
|
+
let out = inner(ctx, url, fetchdef);
|
|
187
|
+
|
|
188
|
+
let res = match &out {
|
|
189
|
+
Ok(r) => r.clone(),
|
|
190
|
+
Err(_) => Value::Noval,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
let (amount, source) = price(options, ctx, &res);
|
|
194
|
+
|
|
195
|
+
// Accumulated on the context, committed once at pre_done. Adding each
|
|
196
|
+
// attempt to the running total and then subtracting it again when a body
|
|
197
|
+
// figure supersedes it loses precision to catastrophic cancellation.
|
|
198
|
+
let pending = match ctx.out_take(COST_PENDING_KEY) {
|
|
199
|
+
Some(OutVal::Val(v)) if matches!(v, Value::Map(_)) => v,
|
|
200
|
+
other => {
|
|
201
|
+
if let Some(o) = other {
|
|
202
|
+
ctx.out_set(COST_PENDING_KEY, o);
|
|
203
|
+
}
|
|
204
|
+
new_pending()
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
let attempts = get_f64(&pending, "attempts").unwrap_or(0.0) + 1.0;
|
|
209
|
+
let total = get_f64(&pending, "amount").unwrap_or(0.0) + amount;
|
|
210
|
+
|
|
211
|
+
// Reported and estimated are kept apart per ATTEMPT: a 503 priced from the
|
|
212
|
+
// rate table followed by a 200 carrying the cost header is part estimate,
|
|
213
|
+
// part reported, and collapsing both into the final attempt's category
|
|
214
|
+
// would corrupt the split.
|
|
215
|
+
//
|
|
216
|
+
// A failed transport is an Err VALUE here, not a panic, so it already
|
|
217
|
+
// reaches this point and is priced from the table or unit - no separate
|
|
218
|
+
// catch is needed, unlike the ts/js ports.
|
|
219
|
+
let bucket = if "header" == source || "body" == source { "reported" } else { "estimated" };
|
|
220
|
+
let prev = get_f64(&pending, bucket).unwrap_or(0.0);
|
|
221
|
+
setp(&pending, bucket, Value::Num(prev + amount));
|
|
222
|
+
|
|
223
|
+
setp(&pending, "attempts", Value::Num(attempts));
|
|
224
|
+
setp(&pending, "amount", Value::Num(total));
|
|
225
|
+
setp(&pending, "source", Value::str(source));
|
|
226
|
+
|
|
227
|
+
let piped = matches!(getp(&pending, "piped"), Value::Bool(true));
|
|
228
|
+
|
|
229
|
+
track.borrow_mut().attempts += 1;
|
|
230
|
+
|
|
231
|
+
// direct() and graphql() reach the transport without dispatching any
|
|
232
|
+
// pipeline hooks, so there is no pre_point to gate on and no pre_done to
|
|
233
|
+
// commit. Their spend is committed here, or it would never be counted.
|
|
234
|
+
if !piped {
|
|
235
|
+
commit(track, options, ctx, &pending, "_", "direct");
|
|
236
|
+
ctx.out_take(COST_PENDING_KEY);
|
|
237
|
+
} else {
|
|
238
|
+
ctx.out_set(COST_PENDING_KEY, OutVal::Val(pending));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
out
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
fn new_pending() -> Value {
|
|
246
|
+
jo(vec![
|
|
247
|
+
("attempts", Value::Num(0.0)),
|
|
248
|
+
("amount", Value::Num(0.0)),
|
|
249
|
+
("reported", Value::Num(0.0)),
|
|
250
|
+
("estimated", Value::Num(0.0)),
|
|
251
|
+
("source", Value::str("none".to_string())),
|
|
252
|
+
("piped", Value::Bool(false)),
|
|
253
|
+
])
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
// Commit one operation's spend: totals, budget, per-op and per-actor
|
|
258
|
+
// attribution, and the record. Shared by finish and the raw-request path in
|
|
259
|
+
// charge, which has no pre_done to reach.
|
|
260
|
+
fn commit(
|
|
261
|
+
track: &Rc<RefCell<CostTrack>>,
|
|
262
|
+
options: &Value,
|
|
263
|
+
ctx: &Rc<Context>,
|
|
264
|
+
pending: &Value,
|
|
265
|
+
entity: &str,
|
|
266
|
+
opname: &str,
|
|
267
|
+
) {
|
|
268
|
+
let attempts = get_f64(pending, "attempts").unwrap_or(0.0);
|
|
269
|
+
let mut amount = get_f64(pending, "amount").unwrap_or(0.0);
|
|
270
|
+
let mut reported = get_f64(pending, "reported").unwrap_or(0.0);
|
|
271
|
+
let mut estimated = get_f64(pending, "estimated").unwrap_or(0.0);
|
|
272
|
+
let mut source = match getp(pending, "source") {
|
|
273
|
+
Value::Str(s) => s,
|
|
274
|
+
_ => "none".to_string(),
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// A body figure prices the whole call, so it replaces the per-attempt
|
|
278
|
+
// estimate rather than adding to it, and being server-stated the whole
|
|
279
|
+
// amount counts as reported.
|
|
280
|
+
if let Some(b) = body_amount(options, ctx) {
|
|
281
|
+
amount = b;
|
|
282
|
+
reported = b;
|
|
283
|
+
estimated = 0.0;
|
|
284
|
+
source = "body".to_string();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
let mut actor = "anonymous".to_string();
|
|
288
|
+
let opt_actor = fopt_str(options, "actor", "");
|
|
289
|
+
if !opt_actor.is_empty() {
|
|
290
|
+
actor = opt_actor;
|
|
291
|
+
}
|
|
292
|
+
{
|
|
293
|
+
let ctrl = ctx.ctrl.borrow().clone();
|
|
294
|
+
let c = ctrl.borrow();
|
|
295
|
+
if !c.actor.is_empty() {
|
|
296
|
+
actor = c.actor.clone();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
let record = jo(vec![]);
|
|
301
|
+
{
|
|
302
|
+
let mut t = track.borrow_mut();
|
|
303
|
+
spend(&mut t, amount, reported, estimated);
|
|
304
|
+
t.calls += 1;
|
|
305
|
+
t.seq += 1;
|
|
306
|
+
|
|
307
|
+
bump(&mut t.ops, format!("{}.{}", entity, opname), amount);
|
|
308
|
+
bump(&mut t.actors, actor.clone(), amount);
|
|
309
|
+
|
|
310
|
+
setp(&record, "seq", Value::Num(t.seq as f64));
|
|
311
|
+
setp(&record, "entity", Value::str(entity.to_string()));
|
|
312
|
+
setp(&record, "op", Value::str(opname.to_string()));
|
|
313
|
+
setp(&record, "actor", Value::str(actor));
|
|
314
|
+
setp(&record, "amount", Value::Num(amount));
|
|
315
|
+
setp(&record, "currency", Value::str(t.currency.clone()));
|
|
316
|
+
setp(&record, "source", Value::str(source));
|
|
317
|
+
setp(&record, "attempts", Value::Num(attempts));
|
|
318
|
+
|
|
319
|
+
t.last = record.clone();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let sink = getp(options, "sink");
|
|
323
|
+
if let Value::Func(_) = sink {
|
|
324
|
+
call_vfn(&sink, &record);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
impl Feature for CostFeature {
|
|
329
|
+
fn name(&self) -> String {
|
|
330
|
+
self.name.clone()
|
|
331
|
+
}
|
|
332
|
+
fn active(&self) -> bool {
|
|
333
|
+
self.active
|
|
334
|
+
}
|
|
335
|
+
fn add_options(&self) -> Option<Value> {
|
|
336
|
+
self.add_opts.clone()
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
fn init(&mut self, ctx: &Rc<Context>, options: &Value) {
|
|
340
|
+
self.options = options.clone();
|
|
341
|
+
self.active = fopt_bool(options, "active", false);
|
|
342
|
+
|
|
343
|
+
let limit = fopt_num(options, "budget", 0.0);
|
|
344
|
+
{
|
|
345
|
+
let mut t = self.track.borrow_mut();
|
|
346
|
+
*t = CostTrack::default();
|
|
347
|
+
t.currency = fopt_str(options, "currency", "USD");
|
|
348
|
+
t.limit = limit;
|
|
349
|
+
t.remaining = limit;
|
|
350
|
+
t.last = Value::Noval;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if !self.active {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
let util = ctx.util();
|
|
358
|
+
let inner: FetcherFn = util.fetcher.borrow().clone();
|
|
359
|
+
let track = self.track.clone();
|
|
360
|
+
let options = options.clone();
|
|
361
|
+
|
|
362
|
+
*util.fetcher.borrow_mut() = Rc::new(move |ctx2, url, fetchdef| {
|
|
363
|
+
charge(&track, &options, ctx2, url, fetchdef, &inner)
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// The budget gate. Runs before endpoint resolution, so a refused call
|
|
368
|
+
// costs nothing at all.
|
|
369
|
+
fn pre_point(&mut self, ctx: &Rc<Context>) {
|
|
370
|
+
if !self.active {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
let (limit, amount, currency) = {
|
|
375
|
+
let t = self.track.borrow();
|
|
376
|
+
(t.limit, t.amount, t.currency.clone())
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
// Mark the context as running through the pipeline, so charge knows a
|
|
380
|
+
// pre_done is coming and does not commit the spend itself.
|
|
381
|
+
let pending = match ctx.out_take(COST_PENDING_KEY) {
|
|
382
|
+
Some(OutVal::Val(v)) if matches!(v, Value::Map(_)) => v,
|
|
383
|
+
other => {
|
|
384
|
+
if let Some(o) = other {
|
|
385
|
+
ctx.out_set(COST_PENDING_KEY, o);
|
|
386
|
+
}
|
|
387
|
+
new_pending()
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
setp(&pending, "piped", Value::Bool(true));
|
|
391
|
+
ctx.out_set(COST_PENDING_KEY, OutVal::Val(pending));
|
|
392
|
+
|
|
393
|
+
if limit <= 0.0 || amount < limit {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
self.track.borrow_mut().exceeded = true;
|
|
398
|
+
|
|
399
|
+
if "deny" != fopt_str(&self.options, "onBudget", "warn") {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
let err = ctx.make_error(
|
|
404
|
+
"cost_budget",
|
|
405
|
+
&format!(
|
|
406
|
+
"Cost budget of {} {} is spent ({} {} used)",
|
|
407
|
+
limit, currency, amount, currency
|
|
408
|
+
),
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
// Short-circuit endpoint resolution; MakePoint surfaces this error
|
|
412
|
+
// before any network activity.
|
|
413
|
+
ctx.out_set("point", OutVal::Err(err));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Attribute the operation's spend once the call is finished.
|
|
417
|
+
fn pre_done(&mut self, ctx: &Rc<Context>) {
|
|
418
|
+
self.finish(ctx, true);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// A failed operation still spent the money. When the pipeline errors,
|
|
422
|
+
// pre_done never runs, so without this the attempts are counted and the
|
|
423
|
+
// spend is not, and a budget could never see the cost of a failed call.
|
|
424
|
+
// Whichever hook fires first consumes the pending entry, so it commits
|
|
425
|
+
// exactly once.
|
|
426
|
+
fn pre_unexpected(&mut self, ctx: &Rc<Context>) {
|
|
427
|
+
self.finish(ctx, false);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
impl CostFeature {
|
|
432
|
+
fn finish(&mut self, ctx: &Rc<Context>, done: bool) {
|
|
433
|
+
if !self.active {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
let pending = match ctx.out_take(COST_PENDING_KEY) {
|
|
438
|
+
Some(OutVal::Val(v)) if matches!(v, Value::Map(_)) => v,
|
|
439
|
+
other => {
|
|
440
|
+
if let Some(o) = other {
|
|
441
|
+
ctx.out_set(COST_PENDING_KEY, o);
|
|
442
|
+
}
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
// A FAILED operation that made no attempt never reached the network:
|
|
448
|
+
// PrePoint creates the pending entry to mark the context as piped, and
|
|
449
|
+
// then the budget gate refuses the call (rbac, or an unresolvable
|
|
450
|
+
// endpoint, short-circuits just as early). Committing it would count a
|
|
451
|
+
// call that never happened and file a zero-amount record as `last`.
|
|
452
|
+
//
|
|
453
|
+
// A SUCCEEDED operation that made no attempt is the opposite case: it was
|
|
454
|
+
// served from the cache. That is a real call, and the fact that it cost
|
|
455
|
+
// nothing is the whole point of ordering cost inside the cache.
|
|
456
|
+
if !done && 0.0 == get_f64(&pending, "attempts").unwrap_or(0.0) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
let (entity, opname) = {
|
|
461
|
+
let op = ctx.op.borrow();
|
|
462
|
+
(op.entity.clone(), op.name.clone())
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
commit(&self.track, &self.options, ctx, &pending, &entity, &opname);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
File without changes
|