usage tracker wip

This commit is contained in:
Brandon Thomas committed 2024-07-21 09:25:04 -04:00
1 parent f17a9a034f
commit e5e93364ae
14 files changed
+365 -14

No files matched your search

Generated
+2
View File
@@ -5563,8 +5563,10 @@ name = "redis_schema"
version = "0.0.1"
dependencies = [
"chrono",
"enums",
"errors",
"my-workspace-hack",
"r2d2_redis",
"tokens",
]
+1
View File
@@ -1 +1,2 @@
pub mod premium_product;
pub mod style_transfer;
@@ -0,0 +1 @@
pub mod premium_product_name;
@@ -0,0 +1,134 @@
use std::collections::BTreeSet;
#[cfg(test)]
use strum::EnumCount;
#[cfg(test)]
use strum::EnumIter;
use utoipa::ToSchema;
/// This enum is not backed by a particular database table.
/// This is used to count premium product uses for free and paid users.
///
/// DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY.
#[cfg_attr(test, derive(EnumIter, EnumCount))]
#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum PremiumProductName {
// NB: These keys are kept short to preserve space
#[serde(rename = "fa")]
FaceAnimator,
// NB: These keys are kept short to preserve space
#[serde(rename = "fm")]
FaceMirror,
// NB: These keys are kept short to preserve space
#[serde(rename = "lip")]
Lipsync,
// NB: These keys are kept short to preserve space
#[serde(rename = "vst")]
VideoStyleTransfer,
}
// TODO(bt, 2022-12-21): This desperately needs MySQL integration tests!
impl_enum_display_and_debug_using_to_str!(PremiumProductName);
//impl_mysql_enum_coders!(PremiumProductName);
//impl_mysql_from_row!(PremiumProductName);
/// NB: Legacy API for older code.
impl PremiumProductName {
pub fn to_str(&self) -> &'static str {
match self {
Self::FaceAnimator => "fa",
Self::FaceMirror => "fm",
Self::Lipsync => "lip",
Self::VideoStyleTransfer => "vst",
}
}
pub fn from_str(value: &str) -> Result<Self, String> {
match value {
"fa" => Ok(Self::FaceAnimator),
"fm" => Ok(Self::FaceMirror),
"lip" => Ok(Self::Lipsync),
"vst" => Ok(Self::VideoStyleTransfer),
_ => Err(format!("Unknown PremiumProductName: {}", value)),
}
}
pub fn all_variants() -> BTreeSet<Self> {
// NB: BTreeSet is sorted
// NB: BTreeSet::from() isn't const, but not worth using LazyStatic, etc.
BTreeSet::from([
Self::FaceAnimator,
Self::FaceMirror,
Self::Lipsync,
Self::VideoStyleTransfer,
])
}
}
#[cfg(test)]
mod tests {
use crate::no_table::premium_product::premium_product_name::PremiumProductName;
use crate::test_helpers::assert_serialization;
mod serde {
use super::*;
#[test]
fn test_serialization() {
assert_serialization(PremiumProductName::FaceAnimator, "fa");
assert_serialization(PremiumProductName::FaceMirror, "fm");
assert_serialization(PremiumProductName::Lipsync, "lip");
assert_serialization(PremiumProductName::VideoStyleTransfer, "vst");
}
mod impl_methods {
use super::*;
#[test]
fn to_str() {
assert_eq!(PremiumProductName::FaceAnimator.to_str(), "fa");
assert_eq!(PremiumProductName::FaceMirror.to_str(), "fm");
assert_eq!(PremiumProductName::Lipsync.to_str(), "lip");
assert_eq!(PremiumProductName::VideoStyleTransfer.to_str(), "vst");
}
#[test]
fn from_str() {
assert_eq!(PremiumProductName::from_str("fa").unwrap(), PremiumProductName::FaceAnimator);
assert_eq!(PremiumProductName::from_str("fm").unwrap(), PremiumProductName::FaceMirror);
assert_eq!(PremiumProductName::from_str("lip").unwrap(), PremiumProductName::Lipsync);
assert_eq!(PremiumProductName::from_str("vst").unwrap(), PremiumProductName::VideoStyleTransfer);
}
}
mod manual_variant_checks {
use super::*;
#[test]
fn all_variants() {
let variants = PremiumProductName::all_variants();
assert_eq!(variants.len(), 4);
}
}
mod mechanical_checks {
use super::*;
#[test]
fn variant_length() {
use strum::IntoEnumIterator;
assert_eq!(PremiumProductName::all_variants().len(), PremiumProductName::iter().len());
}
#[test]
fn round_trip() {
for variant in PremiumProductName::all_variants() {
assert_eq!(variant, PremiumProductName::from_str(variant.to_str()).unwrap());
assert_eq!(variant, PremiumProductName::from_str(&format!("{}", variant)).unwrap());
assert_eq!(variant, PremiumProductName::from_str(&format!("{:?}", variant)).unwrap());
}
}
}
}
}
+4
View File
@@ -17,6 +17,7 @@ normal = ["my-workspace-hack"]
[dependencies]
# Internal
enums = { path = "../../_types/enums" }
errors = { path = "../errors" }
tokens = { path = "../../_types/tokens" }
@@ -26,5 +27,8 @@ my-workspace-hack = { version = "0.1", path = "../../my-workspace-hack" }
# External
chrono = { version = "0.4.22", features = ["serde"] }
# Redis
r2d2_redis = "0.14.0"
[dev-dependencies]
# None yet
@@ -1,3 +1,3 @@
pub mod premium_avt_redis_key;
pub mod premium_ip_address_redis_key;
pub mod premium_user_token_redis_key;
pub mod premium_user_redis_key;
@@ -5,7 +5,8 @@ pub struct PremiumAvtRedisKey(pub String);
impl_string_key!(PremiumAvtRedisKey);
const DURATION : Duration = Duration::milliseconds(1000 * 60 * 60 * 24 * 32); // 32 days
// NB: 62 days to last over a month with enough time for debugging.
const REDIS_KEY_TTL_DURATION: Duration = Duration::milliseconds(1000 * 60 * 60 * 24 * 62);
impl PremiumAvtRedisKey {
pub fn new_for_user(avt: &AnonymousVisitorTrackingToken, time: DateTime<Utc>) -> Self {
@@ -15,7 +16,7 @@ impl PremiumAvtRedisKey {
}
pub fn get_redis_ttl() -> Duration {
DURATION
REDIS_KEY_TTL_DURATION
}
}
@@ -35,6 +36,6 @@ mod tests {
#[test]
fn test_duration() {
assert_eq!(PremiumAvtRedisKey::get_redis_ttl().num_days(), 32);
assert_eq!(PremiumAvtRedisKey::get_redis_ttl().num_days(), 62);
}
}
@@ -4,7 +4,8 @@ pub struct PremiumIpAddressRedisKey(pub String);
impl_string_key!(PremiumIpAddressRedisKey);
const DURATION : Duration = Duration::milliseconds(1000 * 60 * 60 * 24 * 32); // 32 days
// NB: 62 days to last over a month with enough time for debugging.
const REDIS_KEY_TTL_DURATION: Duration = Duration::milliseconds(1000 * 60 * 60 * 24 * 62);
impl PremiumIpAddressRedisKey {
pub fn new_for_user(ip_addr: &str, time: DateTime<Utc>) -> Self {
@@ -14,7 +15,7 @@ impl PremiumIpAddressRedisKey {
}
pub fn get_redis_ttl() -> Duration {
DURATION
REDIS_KEY_TTL_DURATION
}
}
@@ -33,6 +34,6 @@ mod tests {
#[test]
fn test_duration() {
assert_eq!(PremiumIpAddressRedisKey::get_redis_ttl().num_days(), 32);
assert_eq!(PremiumIpAddressRedisKey::get_redis_ttl().num_days(), 62);
}
}
@@ -2,13 +2,14 @@ use chrono::{Datelike, DateTime, Duration, Utc};
use tokens::tokens::users::UserToken;
pub struct PremiumUserTokenRedisKey(pub String);
pub struct PremiumUserRedisKey(pub String);
impl_string_key!(PremiumUserTokenRedisKey);
impl_string_key!(PremiumUserRedisKey);
const DURATION : Duration = Duration::milliseconds(1000 * 60 * 60 * 24 * 32); // 32 days
// NB: 62 days to last over a month with enough time for debugging.
const REDIS_KEY_TTL_DURATION: Duration = Duration::milliseconds(1000 * 60 * 60 * 24 * 62);
impl PremiumUserTokenRedisKey {
impl PremiumUserRedisKey {
pub fn new_for_user(user_token: &UserToken, time: DateTime<Utc>) -> Self {
let month = time.month();
let key = format!("premium:user:{}:{}", user_token.as_str(), month);
@@ -16,7 +17,7 @@ impl PremiumUserTokenRedisKey {
}
pub fn get_redis_ttl() -> Duration {
DURATION
REDIS_KEY_TTL_DURATION
}
}
@@ -30,12 +31,12 @@ mod tests {
fn test_new_for_user() {
let user_token = UserToken::new_from_str("token");
let time = Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap();
let key = PremiumUserTokenRedisKey::new_for_user(&user_token, time);
let key = PremiumUserRedisKey::new_for_user(&user_token, time);
assert_eq!(key.as_str(), "premium:user:token:1");
}
#[test]
fn test_duration() {
assert_eq!(PremiumUserTokenRedisKey::get_redis_ttl().num_days(), 32);
assert_eq!(PremiumUserRedisKey::get_redis_ttl().num_days(), 62);
}
}
@@ -0,0 +1 @@
pub mod premium;
@@ -0,0 +1,5 @@
pub mod premium_user_payload;
// Private
pub (crate) mod product_by_week_store;
pub (crate) mod product_by_week_subkey;
@@ -0,0 +1,76 @@
use std::collections::HashMap;
use std::ops::Add;
use chrono::Utc;
use r2d2_redis::r2d2::PooledConnection;
use r2d2_redis::redis::{Commands};
use r2d2_redis::RedisConnectionManager;
use errors::AnyhowResult;
use crate::keys::premium::premium_user_redis_key::PremiumUserRedisKey;
pub struct PremiumUserPayload {
key: PremiumUserRedisKey,
credits_used: Option<u64>,
}
impl PremiumUserPayload {
pub fn read_from_redis(key: PremiumUserRedisKey, mut redis: PooledConnection<RedisConnectionManager>) -> AnyhowResult<Self> {
let _values : HashMap<String, String> = redis.hgetall(key.as_str())?;
let mut free_uses_per_product_map = HashMap::new();
let keys: Vec<String> = redis.hkeys(key.as_str())?;
for key in keys {
if key == "credits" {
continue;
}
let count: u64 = redis.hget(key.as_str(), "count")?;
free_uses_per_product_map.insert(key, count);
}
Ok(Self {
key,
credits_used: Some(0), // TODO
//free_uses_per_product_map,
})
}
// pub fn persist_to_redis(&self, mut redis: PooledConnection<RedisConnectionManager>) -> AnyhowResult<()> {
// let mut values = Vec::with_capacity(self.free_uses_per_product_map.len() + 1);
//
// values.push(("credits", self.credits_used.unwrap_or(0).to_string()));
//
// for (product_by_week_key, count) in &self.free_uses_per_product_map {
// values.push((product_by_week_key, count.to_string()));
// }
//
// redis.hset_multiple(self.key.as_str(), &values)?;
//
// self.set_key_expiry(redis)?;
//
// Ok(())
// }
pub fn set_key_expiry(&self, mut redis: PooledConnection<RedisConnectionManager>) -> AnyhowResult<()> {
let expire_at= Utc::now()
.add(PremiumUserRedisKey::get_redis_ttl())
.timestamp() as usize;
redis.expire_at(self.key.as_str(), expire_at)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
#[test]
fn todo() {
}
}
@@ -0,0 +1,57 @@
use std::collections::HashMap;
use enums::no_table::premium_product::premium_product_name::PremiumProductName;
use crate::payloads::premium::product_by_week_subkey::ProductByWeekSubkey;
pub struct ProductByWeekStore {
/// Key: "{product_id}:{iso_week_zero_index}"
/// Value: count of uses
free_uses_per_product_map: HashMap<ProductByWeekSubkey, u64>,
}
impl ProductByWeekStore {
pub fn new() -> Self {
Self {
free_uses_per_product_map: HashMap::new(),
}
}
pub fn increment_use(&mut self, name: PremiumProductName, week: u32) {
let key = ProductByWeekSubkey::new(name, week);
let count = self.free_uses_per_product_map.entry(key).or_insert(0);
*count += 1;
}
pub fn get_use_count(&self, name: PremiumProductName, week: u32) -> u64 {
let key = ProductByWeekSubkey::new(name, week);
*self.free_uses_per_product_map.get(&key).unwrap_or(&0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_increment_use() {
let mut store = ProductByWeekStore::new();
store.increment_use(PremiumProductName::FaceAnimator, 10);
store.increment_use(PremiumProductName::FaceAnimator, 10);
store.increment_use(PremiumProductName::FaceAnimator, 10);
store.increment_use(PremiumProductName::FaceMirror, 10);
store.increment_use(PremiumProductName::Lipsync, 11);
// Week 10
assert_eq!(store.get_use_count(PremiumProductName::FaceAnimator, 10), 3);
assert_eq!(store.get_use_count(PremiumProductName::FaceMirror, 10), 1);
assert_eq!(store.get_use_count(PremiumProductName::Lipsync, 10), 0);
assert_eq!(store.get_use_count(PremiumProductName::VideoStyleTransfer, 10), 0);
// Week 11
assert_eq!(store.get_use_count(PremiumProductName::FaceAnimator, 11), 0);
assert_eq!(store.get_use_count(PremiumProductName::FaceMirror, 11), 0);
assert_eq!(store.get_use_count(PremiumProductName::Lipsync, 11), 1);
assert_eq!(store.get_use_count(PremiumProductName::VideoStyleTransfer, 11), 0);
}
}
@@ -0,0 +1,67 @@
use std::num::ParseIntError;
use chrono::{Datelike, Utc};
use enums::no_table::premium_product::premium_product_name::PremiumProductName;
use errors::{anyhow, AnyhowResult};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ProductByWeekSubkey {
pub product_name: PremiumProductName,
pub week: u32,
}
impl ProductByWeekSubkey {
pub fn new(product_name: PremiumProductName, week: u32) -> Self {
Self { product_name, week }
}
pub fn new_this_week(product_name: PremiumProductName) -> Self {
let time = Utc::now();
let week = time.iso_week();
let week = week.week0();
Self { product_name, week }
}
pub fn from_string(key: &str) -> AnyhowResult<Self> {
let mut parts = key.split(':');
let product_name = parts.next()
.ok_or_else(|| anyhow!("No product name"))?;
let product_name = PremiumProductName::from_str(product_name)
.map_err(|err| anyhow!(err))?;
let week = parts.next()
.ok_or_else(|| anyhow!("No week"))?
.parse()
.map_err(|err: ParseIntError| anyhow!(err))?;
Ok(Self {
product_name,
week,
})
}
pub fn to_string(&self) -> String {
format!("{}:{}", self.product_name.to_str(), self.week)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_this_week() {
let key = ProductByWeekSubkey::new_this_week(PremiumProductName::FaceAnimator);
assert_eq!(key.product_name, PremiumProductName::FaceAnimator);
}
#[test]
fn test_from_string() {
let key = ProductByWeekSubkey::from_string("fa:1").unwrap();
assert_eq!(key.product_name, PremiumProductName::FaceAnimator);
assert_eq!(key.week, 1);
}
#[test]
fn test_to_string() {
let key = ProductByWeekSubkey::new(PremiumProductName::FaceMirror, 2);
assert_eq!(key.to_string(), "fm:2");
}
}