Skip to content

Commit 97b31b9

Browse files
Reimplemented the energy tracking for identities (#115)
* Refactoring some stuff for energy * Fix an issue with i128 query params * Infinite budget in Standalone * Energy and crash fixes * Hopefully fixed the test that now has energy * Addresses Centril's comments * Cargo fmt --------- Signed-off-by: Tyler Cloutier <[email protected]>
1 parent 7faca1e commit 97b31b9

File tree

17 files changed

+210
-57
lines changed

17 files changed

+210
-57
lines changed

Cargo.lock

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cli/src/subcommands/energy.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ fn get_energy_subcommands() -> Vec<clap::Command> {
2727
.arg(
2828
Arg::new("balance")
2929
.required(true)
30-
.value_parser(value_parser!(u64))
30+
.value_parser(value_parser!(i128))
3131
.help("The balance value to set"),
3232
)
3333
.arg(
@@ -63,7 +63,7 @@ pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error
6363
async fn exec_update_balance(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
6464
// let project_name = args.value_of("project name").unwrap();
6565
let hex_id = args.get_one::<String>("identity");
66-
let balance = *args.get_one::<u64>("balance").unwrap();
66+
let balance = *args.get_one::<i128>("balance").unwrap();
6767
let quiet = args.get_flag("quiet");
6868

6969
let hex_id = hex_id_or_default(hex_id, &config);
@@ -102,7 +102,7 @@ pub(super) async fn set_balance(
102102
client: &reqwest::Client,
103103
config: &Config,
104104
hex_identity: &str,
105-
balance: u64,
105+
balance: i128,
106106
) -> anyhow::Result<reqwest::Response> {
107107
// TODO: this really should be form data in POST body, not query string parameter, but gotham
108108
// does not support that on the server side without an extension.

crates/client-api/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,4 @@ futures = "0.3"
3232
bytes = "1"
3333
bytestring = "1"
3434
tokio-tungstenite = "0.18.0"
35+
itoa = "1.0.9"

crates/client-api/src/auth.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::fmt::Write;
12
use std::time::Duration;
23

34
use axum::extract::rejection::{TypedHeaderRejection, TypedHeaderRejectionReason};
@@ -6,6 +7,7 @@ use axum::headers::authorization::Credentials;
67
use axum::headers::{self, authorization};
78
use axum::response::IntoResponse;
89
use axum::TypedHeader;
10+
use bytes::BytesMut;
911
use http::{request, HeaderValue, StatusCode};
1012
use serde::Deserialize;
1113
use spacetimedb::auth::identity::{
@@ -243,7 +245,9 @@ impl headers::Header for SpacetimeEnergyUsed {
243245
}
244246

245247
fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
246-
values.extend([self.0 .0.into()])
248+
let mut buf = BytesMut::new();
249+
let _ = buf.write_str(itoa::Buffer::new().format(self.0 .0));
250+
values.extend([HeaderValue::from_bytes(&buf).unwrap()]);
247251
}
248252
}
249253

crates/client-api/src/lib.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ use spacetimedb::client::ClientActorIndex;
77
use spacetimedb::control_db::ControlDb;
88
use spacetimedb::database_instance_context_controller::DatabaseInstanceContextController;
99
use spacetimedb::hash::Hash;
10-
use spacetimedb::host::HostController;
1110
use spacetimedb::host::UpdateDatabaseResult;
11+
use spacetimedb::host::{EnergyQuanta, HostController};
1212
use spacetimedb::identity::Identity;
1313
use spacetimedb::messages::control_db::{Database, DatabaseInstance, HostType, Node};
1414
use spacetimedb::messages::worker_db::DatabaseInstanceState;
@@ -90,6 +90,8 @@ pub trait ControlNodeDelegate: Send + Sync {
9090

9191
async fn alloc_spacetime_identity(&self) -> spacetimedb::control_db::Result<Identity>;
9292

93+
async fn withdraw_energy(&self, identity: &Identity, amount: EnergyQuanta) -> spacetimedb::control_db::Result<()>;
94+
9395
fn public_key(&self) -> &DecodingKey;
9496
fn private_key(&self) -> &EncodingKey;
9597
}
@@ -123,6 +125,10 @@ impl<T: ControlNodeDelegate + ?Sized> ControlNodeDelegate for ArcEnv<T> {
123125
self.0.alloc_spacetime_identity().await
124126
}
125127

128+
async fn withdraw_energy(&self, identity: &Identity, amount: EnergyQuanta) -> spacetimedb::control_db::Result<()> {
129+
self.0.withdraw_energy(identity, amount).await
130+
}
131+
126132
fn public_key(&self) -> &DecodingKey {
127133
self.0.public_key()
128134
}
@@ -141,6 +147,10 @@ impl<T: ControlNodeDelegate + ?Sized> ControlNodeDelegate for Arc<T> {
141147
(**self).alloc_spacetime_identity().await
142148
}
143149

150+
async fn withdraw_energy(&self, identity: &Identity, amount: EnergyQuanta) -> spacetimedb::control_db::Result<()> {
151+
(**self).withdraw_energy(identity, amount).await
152+
}
153+
144154
fn public_key(&self) -> &DecodingKey {
145155
(**self).public_key()
146156
}

crates/client-api/src/routes/energy.rs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use http::StatusCode;
66
use serde::Deserialize;
77
use serde_json::json;
88

9+
use spacetimedb::host::EnergyQuanta;
910
use spacetimedb_lib::Identity;
1011

1112
use crate::auth::SpacetimeAuthHeader;
@@ -28,18 +29,20 @@ pub async fn get_energy_balance(
2829
let balance = ctx
2930
.control_db()
3031
.get_energy_balance(&identity)
31-
.await
3232
.map_err(log_and_500)?
33-
.ok_or((StatusCode::NOT_FOUND, "No budget for identity"))?;
33+
.unwrap_or(EnergyQuanta(0));
3434

35-
let response_json = json!({ "balance": balance });
35+
let response_json = json!({
36+
// Note: balance must be returned as a string to avoid truncation.
37+
"balance": balance.0.to_string(),
38+
});
3639

3740
Ok(axum::Json(response_json))
3841
}
3942

4043
#[derive(Deserialize)]
4144
pub struct SetEnergyBalanceQueryParams {
42-
balance: Option<i64>,
45+
balance: Option<String>,
4346
}
4447
pub async fn set_energy_balance(
4548
State(ctx): State<Arc<dyn ControlCtx>>,
@@ -61,15 +64,23 @@ pub async fn set_energy_balance(
6164

6265
let identity = Identity::from(identity);
6366

64-
let balance = balance.unwrap_or(0);
67+
let balance = balance
68+
.map(|balance| balance.parse::<i128>())
69+
.transpose()
70+
.map_err(|err| {
71+
log::error!("Failed to parse balance: {:?}", err);
72+
StatusCode::BAD_REQUEST
73+
})?;
74+
let balance = EnergyQuanta(balance.unwrap_or(0));
6575

6676
ctx.control_db()
6777
.set_energy_balance(identity, balance)
78+
.await
6879
.map_err(log_and_500)?;
6980

70-
// Return the modified budget.
7181
let response_json = json!({
72-
"balance": balance,
82+
// Note: balance must be returned as a string to avoid truncation.
83+
"balance": balance.0.to_string(),
7384
});
7485

7586
Ok(axum::Json(response_json))

crates/core/src/control_db.rs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::address::Address;
22

33
use crate::hash::hash_bytes;
4+
use crate::host::EnergyQuanta;
45
use crate::identity::Identity;
56
use crate::messages::control_db::{Database, DatabaseInstance, EnergyBalance, IdentityEmail, Node};
67
use crate::stdb_path;
@@ -34,6 +35,8 @@ pub enum Error {
3435
DecodingError(#[from] bsatn::DecodeError),
3536
#[error(transparent)]
3637
DomainParsingError(#[from] DomainParsingError),
38+
#[error("connection error")]
39+
ConnectionError(),
3740
#[error(transparent)]
3841
JSONDeserializationError(#[from] serde_json::Error),
3942
}
@@ -515,10 +518,10 @@ impl ControlDb {
515518
continue;
516519
}
517520
};
518-
let Ok(arr) = <[u8; 8]>::try_from(balance_entry.1.as_ref()) else {
521+
let Ok(arr) = <[u8; 16]>::try_from(balance_entry.1.as_ref()) else {
519522
return Err(Error::DecodingError(bsatn::DecodeError::BufferLength));
520523
};
521-
let balance = i64::from_ne_bytes(arr);
524+
let balance = i128::from_ne_bytes(arr);
522525
let energy_balance = EnergyBalance {
523526
identity: Identity::from_slice(balance_entry.0.iter().as_slice()),
524527
balance,
@@ -531,16 +534,16 @@ impl ControlDb {
531534
/// Return the current budget for a given identity as stored in the db.
532535
/// Note: this function is for the stored budget only and should *only* be called by functions in
533536
/// `control_budget`, where a cached copy is stored along with business logic for managing it.
534-
pub async fn get_energy_balance(&self, identity: &Identity) -> Result<Option<i64>> {
537+
pub fn get_energy_balance(&self, identity: &Identity) -> Result<Option<EnergyQuanta>> {
535538
let tree = self.db.open_tree("energy_budget")?;
536539
let key = identity.to_hex();
537540
let value = tree.get(key.as_bytes())?;
538541
if let Some(value) = value {
539-
let Ok(arr) = <[u8; 8]>::try_from(value.as_ref()) else {
542+
let Ok(arr) = <[u8; 16]>::try_from(value.as_ref()) else {
540543
return Err(Error::DecodingError(bsatn::DecodeError::BufferLength));
541544
};
542-
let balance = i64::from_ne_bytes(arr);
543-
Ok(Some(balance))
545+
let balance = i128::from_be_bytes(arr);
546+
Ok(Some(EnergyQuanta(balance)))
544547
} else {
545548
Ok(None)
546549
}
@@ -549,10 +552,10 @@ impl ControlDb {
549552
/// Update the stored current budget for a identity.
550553
/// Note: this function is for the stored budget only and should *only* be called by functions in
551554
/// `control_budget`, where a cached copy is stored along with business logic for managing it.
552-
pub fn set_energy_balance(&self, identity: Identity, energy_balance: i64) -> Result<()> {
555+
pub async fn set_energy_balance(&self, identity: Identity, energy_balance: EnergyQuanta) -> Result<()> {
553556
let tree = self.db.open_tree("energy_budget")?;
554557
let key = identity.to_hex();
555-
tree.insert(key, &energy_balance.to_be_bytes())?;
558+
tree.insert(key, &energy_balance.0.to_be_bytes())?;
556559

557560
Ok(())
558561
}

crates/core/src/host/host_controller.rs

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use super::{EnergyMonitor, NullEnergyMonitor, ReducerArgs};
1818

1919
pub struct HostController {
2020
modules: Mutex<HashMap<u64, ModuleHost>>,
21-
energy_monitor: Arc<dyn EnergyMonitor>,
21+
pub energy_monitor: Arc<dyn EnergyMonitor>,
2222
}
2323

2424
#[derive(PartialEq, Eq, Hash, Copy, Clone, Serialize, Debug)]
@@ -58,20 +58,48 @@ impl fmt::Display for DescribedEntityType {
5858
}
5959
}
6060

61+
/// [EnergyQuanta] represents an amount of energy in a canonical unit.
62+
/// It represents the smallest unit of energy that can be used to pay for
63+
/// a reducer invocation. We will likely refer to this unit as an "eV".
64+
///
65+
/// NOTE: This is represented by a signed integer, because it is possible
66+
/// for a user's balance to go negative. This is allowable
67+
/// for reasons of eventual consistency motivated by performance.
6168
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
62-
pub struct EnergyQuanta(pub u64);
69+
pub struct EnergyQuanta(pub i128);
6370

6471
impl EnergyQuanta {
6572
pub const ZERO: Self = EnergyQuanta(0);
6673

6774
pub const DEFAULT_BUDGET: Self = EnergyQuanta(1_000_000_000_000_000_000);
75+
76+
/// A conversion function to convert from the canonical unit to points used
77+
/// by Wasmer to track energy usage.
78+
pub fn as_points(&self) -> u64 {
79+
if self.0 < 0 {
80+
return 0;
81+
} else if self.0 > u64::MAX as i128 {
82+
return u64::MAX;
83+
}
84+
self.0 as u64
85+
}
86+
87+
/// A conversion function to convert from point used
88+
/// by Wasmer to track energy usage, to our canonical unit.
89+
pub fn from_points(points: u64) -> Self {
90+
Self(points as i128)
91+
}
6892
}
6993

7094
#[derive(Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
71-
pub struct EnergyDiff(pub u64);
95+
pub struct EnergyDiff(pub i128);
7296

7397
impl EnergyDiff {
7498
pub const ZERO: Self = EnergyDiff(0);
99+
100+
pub fn as_quanta(self) -> EnergyQuanta {
101+
EnergyQuanta(self.0)
102+
}
75103
}
76104

77105
impl Sub for EnergyQuanta {

crates/core/src/host/wasmer/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ pub fn make_actor(
3636
// before calling reducer?
3737
// I believe we can just set this to be zero and it's already being set by reducers
3838
// but I don't want to break things, so I'm going to leave it.
39-
let initial_points = EnergyQuanta::DEFAULT_BUDGET;
40-
let metering = Arc::new(Metering::new(initial_points.0, cost_function));
39+
let initial_points = EnergyQuanta::DEFAULT_BUDGET.as_points();
40+
let metering = Arc::new(Metering::new(initial_points, cost_function));
4141

4242
// let mut compiler_config = wasmer_compiler_llvm::LLVM::default();
4343
// compiler_config.opt_level(wasmer_compiler_llvm::LLVMOptLevel::Aggressive);

crates/core/src/host/wasmer/wasmer_module.rs

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,14 @@ use wasmer::{
1111
};
1212
use wasmer_middlewares::metering as wasmer_metering;
1313

14-
fn get_remaining_energy(ctx: &mut impl AsStoreMut, instance: &Instance) -> EnergyQuanta {
14+
fn get_remaining_points(ctx: &mut impl AsStoreMut, instance: &Instance) -> u64 {
1515
let remaining_points = wasmer_metering::get_remaining_points(ctx, instance);
1616
match remaining_points {
17-
wasmer_metering::MeteringPoints::Remaining(x) => EnergyQuanta(x),
18-
wasmer_metering::MeteringPoints::Exhausted => EnergyQuanta::ZERO,
17+
wasmer_metering::MeteringPoints::Remaining(x) => x,
18+
wasmer_metering::MeteringPoints::Exhausted => 0,
1919
}
2020
}
2121

22-
fn set_remaining_energy(ctx: &mut impl AsStoreMut, instance: &Instance, energy: EnergyQuanta) {
23-
wasmer_metering::set_remaining_points(ctx, instance, energy.0)
24-
}
25-
2622
fn log_traceback(func_type: &str, func: &str, e: &RuntimeError) {
2723
let frames = e.trace();
2824
let frames_len = frames.len();
@@ -181,8 +177,8 @@ impl module_host_actor::WasmInstancePre for WasmerModule {
181177
env.as_mut(&mut store).mem = Some(mem);
182178

183179
// Note: this budget is just for initializers
184-
let budget = EnergyQuanta::DEFAULT_BUDGET;
185-
set_remaining_energy(&mut store, &instance, budget);
180+
let budget = EnergyQuanta::DEFAULT_BUDGET.as_points();
181+
wasmer_metering::set_remaining_points(&mut store, &instance, budget);
186182

187183
for preinit in &func_names.preinits {
188184
let func = instance.exports.get_typed_function::<(), ()>(&store, preinit).unwrap();
@@ -316,7 +312,8 @@ impl WasmerInstance {
316312
) -> module_host_actor::ExecuteResult<RuntimeError> {
317313
let store = &mut self.store;
318314
let instance = &self.instance;
319-
set_remaining_energy(store, instance, budget);
315+
let budget = budget.as_points();
316+
wasmer_metering::set_remaining_points(store, instance, budget);
320317

321318
let reduce = instance
322319
.exports
@@ -348,10 +345,10 @@ impl WasmerInstance {
348345
// .call(store, sender_buf.ptr.cast(), timestamp, args_buf.ptr, args_buf.len)
349346
// .and_then(|_| {});
350347
let duration = start.elapsed();
351-
let remaining = get_remaining_energy(store, instance);
348+
let remaining = get_remaining_points(store, instance);
352349
let energy = module_host_actor::EnergyStats {
353-
used: budget - remaining,
354-
remaining,
350+
used: EnergyQuanta::from_points(budget) - EnergyQuanta::from_points(remaining),
351+
remaining: EnergyQuanta::from_points(remaining),
355352
};
356353
module_host_actor::ExecuteResult {
357354
energy,

0 commit comments

Comments
 (0)