Skip to content

Commit 19ccd17

Browse files
authored
Merge pull request #418 from AdExNetwork/redis-and-tests
Issue #415 Create/Modify Campaigns - Redis and tests
2 parents b664dd1 + 75a5a5f commit 19ccd17

File tree

9 files changed

+684
-244
lines changed

9 files changed

+684
-244
lines changed

primitives/src/sentry.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,26 @@ pub mod campaign_create {
361361
}
362362
}
363363

364+
/// This implementation helps with test setup
365+
/// **NOTE:** It erases the CampaignId, since the creation of the campaign gives it's CampaignId
366+
impl From<Campaign> for CreateCampaign {
367+
fn from(campaign: Campaign) -> Self {
368+
Self {
369+
channel: campaign.channel,
370+
creator: campaign.creator,
371+
budget: campaign.budget,
372+
validators: campaign.validators,
373+
title: campaign.title,
374+
pricing_bounds: campaign.pricing_bounds,
375+
event_submission: campaign.event_submission,
376+
ad_units: campaign.ad_units,
377+
targeting_rules: campaign.targeting_rules,
378+
created: campaign.created,
379+
active: campaign.active,
380+
}
381+
}
382+
}
383+
364384
// All editable fields stored in one place, used for checking when a budget is changed
365385
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
366386
pub struct ModifyCampaign {
@@ -403,7 +423,7 @@ pub mod campaign_create {
403423
if let Some(new_pricing_bounds) = self.pricing_bounds {
404424
campaign.pricing_bounds = Some(new_pricing_bounds);
405425
}
406-
426+
407427
if let Some(new_event_submission) = self.event_submission {
408428
campaign.event_submission = Some(new_event_submission);
409429
}

primitives/src/sentry/accounting.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,16 @@ impl<S: BalancesState> Balances<S> {
6868

6969
Ok(())
7070
}
71+
72+
/// Adds the spender to the Balances with `UnifiedNum::from(0)` if he does not exist
73+
pub fn add_spender(&mut self, spender: Address) {
74+
self.spenders.entry(spender).or_insert(UnifiedNum::from(0));
75+
}
76+
77+
/// Adds the earner to the Balances with `UnifiedNum::from(0)` if he does not exist
78+
pub fn add_earner(&mut self, earner: Address) {
79+
self.earners.entry(earner).or_insert(UnifiedNum::from(0));
80+
}
7181
}
7282

7383
#[derive(Debug)]

primitives/src/validator.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ impl From<&Address> for ValidatorId {
3939
}
4040
}
4141

42+
impl From<Address> for ValidatorId {
43+
fn from(address: Address) -> Self {
44+
Self(address)
45+
}
46+
}
47+
4248
impl From<&[u8; 20]> for ValidatorId {
4349
fn from(bytes: &[u8; 20]) -> Self {
4450
Self(Address::from(bytes))

sentry/src/db/accounting.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,7 @@ pub async fn get_accounting_spent(
3838
Ok(row.get("spent"))
3939
}
4040

41-
// TODO This is still WIP
42-
#[allow(dead_code)]
43-
async fn insert_accounting(
41+
pub async fn insert_accounting(
4442
pool: DbPool,
4543
channel: Channel,
4644
balances: Balances<CheckedState>,

sentry/src/db/campaign.rs

Lines changed: 282 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,16 @@ use crate::db::{DbPool, PoolError};
22
use primitives::{Campaign, CampaignId, ChannelId};
33
use tokio_postgres::types::Json;
44

5+
pub use campaign_remaining::CampaignRemaining;
6+
7+
/// ```text
8+
/// INSERT INTO campaigns (id, channel_id, channel, creator, budget, validators, title, pricing_bounds, event_submission, ad_units, targeting_rules, created, active_from, active_to)
9+
/// VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
10+
/// ```
511
pub async fn insert_campaign(pool: &DbPool, campaign: &Campaign) -> Result<bool, PoolError> {
612
let client = pool.get().await?;
713
let ad_units = Json(campaign.ad_units.clone());
8-
let stmt = client.prepare("INSERT INTO campaigns (id, channel_id, channel, creator, budget, validators, title, pricing_bounds, event_submission, ad_units, targeting_rules, created, active_from, active_to) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)").await?;
14+
let stmt = client.prepare("INSERT INTO campaigns (id, channel_id, channel, creator, budget, validators, title, pricing_bounds, event_submission, ad_units, targeting_rules, created, active_from, active_to) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)").await?;
915
let inserted = client
1016
.execute(
1117
&stmt,
@@ -49,6 +55,10 @@ pub async fn fetch_campaign(
4955
}
5056

5157
// TODO: We might need to use LIMIT to implement pagination
58+
/// ```text
59+
/// SELECT id, channel, creator, budget, validators, title, pricing_bounds, event_submission, ad_units, targeting_rules, created, active_from, active_to
60+
/// FROM campaigns WHERE channel_id = $1
61+
/// ```
5262
pub async fn get_campaigns_by_channel(
5363
pool: &DbPool,
5464
channel_id: &ChannelId,
@@ -95,6 +105,277 @@ pub async fn update_campaign(pool: &DbPool, campaign: &Campaign) -> Result<Campa
95105
Ok(Campaign::from(&updated_row))
96106
}
97107

108+
/// struct that handles redis calls for the Campaign Remaining Budget
109+
mod campaign_remaining {
110+
use crate::db::RedisError;
111+
use primitives::{CampaignId, UnifiedNum};
112+
use redis::aio::MultiplexedConnection;
113+
114+
#[derive(Clone)]
115+
pub struct CampaignRemaining {
116+
redis: MultiplexedConnection,
117+
}
118+
119+
impl CampaignRemaining {
120+
pub const CAMPAIGN_REMAINING_KEY: &'static str = "campaignRemaining";
121+
122+
pub fn get_key(campaign: CampaignId) -> String {
123+
format!("{}:{}", Self::CAMPAIGN_REMAINING_KEY, campaign)
124+
}
125+
126+
pub fn new(redis: MultiplexedConnection) -> Self {
127+
Self { redis }
128+
}
129+
130+
pub async fn set_initial(
131+
&self,
132+
campaign: CampaignId,
133+
amount: UnifiedNum,
134+
) -> Result<bool, RedisError> {
135+
redis::cmd("SETNX")
136+
.arg(&Self::get_key(campaign))
137+
.arg(amount.to_u64())
138+
.query_async(&mut self.redis.clone())
139+
.await
140+
}
141+
142+
pub async fn get_remaining_opt(
143+
&self,
144+
campaign: CampaignId,
145+
) -> Result<Option<i64>, RedisError> {
146+
redis::cmd("GET")
147+
.arg(&Self::get_key(campaign))
148+
.query_async::<_, Option<i64>>(&mut self.redis.clone())
149+
.await
150+
}
151+
152+
/// This method uses `max(0, value)` to clamp the value of a campaign, which can be negative and uses `i64`.
153+
/// In addition, it defaults the campaign keys that were not found to `0`.
154+
pub async fn get_multiple(
155+
&self,
156+
campaigns: &[CampaignId],
157+
) -> Result<Vec<UnifiedNum>, RedisError> {
158+
// `MGET` fails on empty keys
159+
if campaigns.is_empty() {
160+
return Ok(vec![]);
161+
}
162+
163+
let keys: Vec<String> = campaigns
164+
.iter()
165+
.map(|campaign| Self::get_key(*campaign))
166+
.collect();
167+
168+
let campaigns_remaining = redis::cmd("MGET")
169+
.arg(keys)
170+
.query_async::<_, Vec<Option<i64>>>(&mut self.redis.clone())
171+
.await?
172+
.into_iter()
173+
.map(|remaining| match remaining {
174+
Some(remaining) => UnifiedNum::from_u64(remaining.max(0).unsigned_abs()),
175+
None => UnifiedNum::from_u64(0),
176+
})
177+
.collect();
178+
179+
Ok(campaigns_remaining)
180+
}
181+
182+
pub async fn increase_by(
183+
&self,
184+
campaign: CampaignId,
185+
amount: UnifiedNum,
186+
) -> Result<i64, RedisError> {
187+
let key = Self::get_key(campaign);
188+
redis::cmd("INCRBY")
189+
.arg(&key)
190+
.arg(amount.to_u64())
191+
.query_async(&mut self.redis.clone())
192+
.await
193+
}
194+
195+
pub async fn decrease_by(
196+
&self,
197+
campaign: CampaignId,
198+
amount: UnifiedNum,
199+
) -> Result<i64, RedisError> {
200+
let key = Self::get_key(campaign);
201+
redis::cmd("DECRBY")
202+
.arg(&key)
203+
.arg(amount.to_u64())
204+
.query_async(&mut self.redis.clone())
205+
.await
206+
}
207+
}
208+
209+
#[cfg(test)]
210+
mod test {
211+
use primitives::util::tests::prep_db::DUMMY_CAMPAIGN;
212+
213+
use crate::db::redis_pool::TESTS_POOL;
214+
215+
use super::*;
216+
217+
#[tokio::test]
218+
async fn it_sets_initial_increases_and_decreases_remaining_for_campaign() {
219+
let redis = TESTS_POOL.get().await.expect("Should return Object");
220+
221+
let campaign = DUMMY_CAMPAIGN.id;
222+
let campaign_remaining = CampaignRemaining::new(redis.connection.clone());
223+
224+
// Get remaining on a key which was not set
225+
{
226+
let get_remaining = campaign_remaining
227+
.get_remaining_opt(campaign)
228+
.await
229+
.expect("Should get None");
230+
231+
assert_eq!(None, get_remaining);
232+
}
233+
234+
// Set Initial amount on that key
235+
{
236+
let initial_amount = UnifiedNum::from(1_000_u64);
237+
let set_initial = campaign_remaining
238+
.set_initial(campaign, initial_amount)
239+
.await
240+
.expect("Should set value in redis");
241+
assert!(set_initial);
242+
243+
// get the remaining of that key, should be the initial value
244+
let get_remaining = campaign_remaining
245+
.get_remaining_opt(campaign)
246+
.await
247+
.expect("Should get None");
248+
249+
assert_eq!(
250+
Some(1_000_i64),
251+
get_remaining,
252+
"should return the initial value that was set"
253+
);
254+
}
255+
256+
// Set initial on already existing key, should return `false`
257+
{
258+
let set_failing_initial = campaign_remaining
259+
.set_initial(campaign, UnifiedNum::from(999_u64))
260+
.await
261+
.expect("Should set value in redis");
262+
assert!(!set_failing_initial);
263+
}
264+
265+
// Decrease by amount
266+
{
267+
let decrease_amount = UnifiedNum::from(64);
268+
let decrease_by = campaign_remaining
269+
.decrease_by(campaign, decrease_amount)
270+
.await
271+
.expect("Should decrease remaining amount");
272+
273+
assert_eq!(936_i64, decrease_by);
274+
}
275+
276+
// Increase by amount
277+
{
278+
let increase_amount = UnifiedNum::from(1064);
279+
let increase_by = campaign_remaining
280+
.increase_by(campaign, increase_amount)
281+
.await
282+
.expect("Should increase remaining amount");
283+
284+
assert_eq!(2_000_i64, increase_by);
285+
}
286+
287+
let get_remaining = campaign_remaining
288+
.get_remaining_opt(campaign)
289+
.await
290+
.expect("Should get remaining");
291+
292+
assert_eq!(Some(2_000_i64), get_remaining);
293+
294+
// Decrease by amount > than currently set
295+
{
296+
let decrease_amount = UnifiedNum::from(5_000);
297+
let decrease_by = campaign_remaining
298+
.decrease_by(campaign, decrease_amount)
299+
.await
300+
.expect("Should decrease remaining amount");
301+
302+
assert_eq!(-3_000_i64, decrease_by);
303+
}
304+
305+
// Increase the negative value without going > 0
306+
{
307+
let increase_amount = UnifiedNum::from(1000);
308+
let increase_by = campaign_remaining
309+
.increase_by(campaign, increase_amount)
310+
.await
311+
.expect("Should increase remaining amount");
312+
313+
assert_eq!(-2_000_i64, increase_by);
314+
}
315+
}
316+
317+
#[tokio::test]
318+
async fn it_gets_multiple_campaigns_remaining() {
319+
let redis = TESTS_POOL.get().await.expect("Should return Object");
320+
let campaign_remaining = CampaignRemaining::new(redis.connection.clone());
321+
322+
// get multiple with empty campaigns slice
323+
// `MGET` throws error on an empty keys argument
324+
assert!(
325+
campaign_remaining
326+
.get_multiple(&[])
327+
.await
328+
.expect("Should get multiple")
329+
.is_empty(),
330+
"Should return an empty result"
331+
);
332+
333+
let campaigns = (CampaignId::new(), CampaignId::new(), CampaignId::new());
334+
335+
// set initial amounts
336+
{
337+
assert!(campaign_remaining
338+
.set_initial(campaigns.0, UnifiedNum::from(100))
339+
.await
340+
.expect("Should set value in redis"));
341+
342+
assert!(campaign_remaining
343+
.set_initial(campaigns.1, UnifiedNum::from(200))
344+
.await
345+
.expect("Should set value in redis"));
346+
347+
assert!(campaign_remaining
348+
.set_initial(campaigns.2, UnifiedNum::from(300))
349+
.await
350+
.expect("Should set value in redis"));
351+
}
352+
353+
// set campaigns.1 to negative value, should return `0` because of `max(value, 0)`
354+
assert_eq!(
355+
-300_i64,
356+
campaign_remaining
357+
.decrease_by(campaigns.1, UnifiedNum::from(500))
358+
.await
359+
.expect("Should decrease remaining")
360+
);
361+
362+
let multiple = campaign_remaining
363+
.get_multiple(&[campaigns.0, campaigns.1, campaigns.2])
364+
.await
365+
.expect("Should get multiple");
366+
367+
assert_eq!(
368+
vec![
369+
UnifiedNum::from(100),
370+
UnifiedNum::from(0),
371+
UnifiedNum::from(300)
372+
],
373+
multiple
374+
);
375+
}
376+
}
377+
}
378+
98379
#[cfg(test)]
99380
mod test {
100381
use primitives::{

0 commit comments

Comments
 (0)