MCaptcha: increment, decrement & rdb persist

This commit is contained in:
realaravinth 2021-06-06 16:35:14 +05:30
parent fd9b6f75ae
commit d3bce7669a
No known key found for this signature in database
GPG key ID: AD9F0F08E855ED88
13 changed files with 274 additions and 146 deletions

3
Cargo.lock generated
View file

@ -303,7 +303,7 @@ dependencies = [
[[package]] [[package]]
name = "libmcaptcha" name = "libmcaptcha"
version = "0.1.4" version = "0.1.4"
source = "git+https://github.com/mCaptcha/libmcaptcha?branch=master#895d54d26d7325f83df963794c815b00f19990d0" source = "git+https://github.com/mCaptcha/libmcaptcha?branch=master#68f95f99c28753a7725cd4107078978477ed2f63"
dependencies = [ dependencies = [
"derive_builder", "derive_builder",
"derive_more", "derive_more",
@ -324,6 +324,7 @@ dependencies = [
name = "mcaptcha-cache" name = "mcaptcha-cache"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"derive_more",
"lazy_static", "lazy_static",
"libc", "libc",
"libmcaptcha", "libmcaptcha",

View file

@ -19,6 +19,7 @@ serde_json = "1.0.64"
serde = {version = "1.0.126", features = ["derive"]} serde = {version = "1.0.126", features = ["derive"]}
lazy_static = "1.4.0" lazy_static = "1.4.0"
rand = "0.8.3" rand = "0.8.3"
derive_more = "0.99"
libmcaptcha = { branch = "master", git = "https://github.com/mCaptcha/libmcaptcha", features = ["minimal"], default-features = false } libmcaptcha = { branch = "master", git = "https://github.com/mCaptcha/libmcaptcha", features = ["minimal"], default-features = false }
#libmcaptcha = { path = "../libmcaptcha", features = ["minimal"], default-features = false} #libmcaptcha = { path = "../libmcaptcha", features = ["minimal"], default-features = false}

41
docs/mechanism.md Normal file
View file

@ -0,0 +1,41 @@
# mCaptcha Redis Module Mechanism
## Data types
1. Bucket
2. Bucket safety
3. mCaptcha
4. mCaptcha safety
## Bucket
- Timer queue, used for scheduling decrements.
- Timer used for scheduling can't be persisted so requires a safety
## Bucket Safety
- Has expiry timer(Redis `EXIPRE`)
- Key name includes bucket name for which this safety was created for
- When Redis is started after crash, bucket safety expires and expire
event is fired, the corresponding bucket is fetched and executed.
## mCaptcha
- Contains mCaptcha defense details and current state
- When redis performs resharding, it's possible that mCaptcha gets
separated from its bucket.
- We can't pin mCaptcha by using hashtags because the client figures out
where a key should be placed/is available and will have no
knowledge about node IDs that we use for pinning.
- So this too requires a safety to make sure that when recovering from a
crash, it's counter doesn't have residues permanently.
## mCaptcha safety
- Has expire timer(Redis `EXIPRE`)
- Key name includes mCaptcha name for which this safety was created for
- when expiry event is fired for this type, a bucket for corresponding
mCaptcha is created to decrement it by `x`(where `x` is the count of mCaptcha
when this event is fired). It's not perfect but at least we get
eventual consistency.

View file

@ -28,6 +28,7 @@ use redis_module::{raw, Context};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::errors::*; use crate::errors::*;
use crate::mcaptcha::MCaptcha;
use crate::utils::*; use crate::utils::*;
use crate::*; use crate::*;
@ -58,7 +59,7 @@ pub struct Bucket {
/// instant(seconds from UNIX_EPOCH) at which time bucket begins decrement process /// instant(seconds from UNIX_EPOCH) at which time bucket begins decrement process
bucket_instant: u64, bucket_instant: u64,
/// a list of captcha keys that should be decremented during clean up /// a list of captcha keys that should be decremented during clean up
decrement: HashMap<String, usize>, decrement: HashMap<String, u32>,
} }
impl Bucket { impl Bucket {
@ -109,6 +110,54 @@ impl Bucket {
Ok(bucket) Ok(bucket)
} }
/// decrement runner that decrements all registered counts _without_ cleaning after itself
/// use [decrement] when you require auto cleanup. Internally, it calls this method.
#[inline]
fn decrement_runner(ctx: &Context, key: &RedisKeyWritable) {
let val = key.get_value::<Bucket>(&MCAPTCHA_BUCKET_TYPE).unwrap();
match val {
Some(bucket) => {
ctx.log_debug(&format!("entering loop hashmap "));
for (captcha, count) in bucket.decrement.drain() {
ctx.log_debug(&format!(
"reading captcha: {} with decr count {}",
&captcha, count
));
let stored_captcha = ctx.open_key_writable(&captcha);
if stored_captcha.key_type() == KeyType::Empty {
continue;
}
let captcha = MCaptcha::get_mcaptcha(&ctx, &stored_captcha)
.unwrap()
.unwrap();
captcha.decrement_visitor_by(count);
}
}
None => {
ctx.log_debug(&format!("bucket not found, can't decrement"));
}
}
}
/// executes when timer goes off. Decrements all registered counts and cleans itself up
fn decrement(ctx: &Context, bucket_instant: u64) {
// get bucket
let bucket_name = get_bucket_name(bucket_instant);
let timer = ctx.open_key_writable(&get_timer_name_from_bucket_name(&bucket_name));
let _ = timer.delete();
ctx.log_debug(&format!("Bucket instant: {}", &bucket_instant));
let bucket = ctx.open_key_writable(&bucket_name);
Bucket::decrement_runner(ctx, &bucket);
match bucket.delete() {
Err(e) => ctx.log_warning(&format!("enountered error while deleting hashmap: {:?}", e)),
Ok(_) => (),
}
}
/// increments count of key = captcha and registers for auto decrement /// increments count of key = captcha and registers for auto decrement
#[inline] #[inline]
fn increment(ctx: &Context, duration: u64, captcha: &str) -> CacheResult<()> { fn increment(ctx: &Context, duration: u64, captcha: &str) -> CacheResult<()> {
@ -116,20 +165,11 @@ impl Bucket {
ctx.log_debug(&captcha_name); ctx.log_debug(&captcha_name);
// increment // increment
let captcha = ctx.open_key_writable(&captcha_name); let captcha = ctx.open_key_writable(&captcha_name);
let captcha = MCaptcha::get_mcaptcha(ctx, &captcha)?;
match captcha.read()? { match captcha {
Some(val) => { Some(val) => val.add_visitor(),
if val.trim().is_empty() { None => return Err(CacheError::new("Captcha not found".into())),
captcha.write("1")?;
} else {
let mut val: usize = val.parse()?;
val += 1;
captcha.write(&val.to_string())?;
}
}
None => {
captcha.write("1")?;
}
} }
let bucket_instant = get_bucket_instant(duration)?; let bucket_instant = get_bucket_instant(duration)?;
@ -161,65 +201,6 @@ impl Bucket {
Ok(()) Ok(())
} }
/// decrement runner that decrements all registered counts _without_ cleaning after itself
/// use [decrement] when you require auto cleanup. Internally, it calls this method.
#[inline]
fn decrement_runner(ctx: &Context, key: &RedisKeyWritable) {
let val = key.get_value::<Bucket>(&MCAPTCHA_BUCKET_TYPE).unwrap();
match val {
Some(bucket) => {
ctx.log_debug(&format!("entering loop hashmap "));
for (captcha, count) in bucket.decrement.drain() {
ctx.log_debug(&format!(
"reading captcha: {} with decr count {}",
&captcha, count
));
let stored_captcha = ctx.open_key_writable(&captcha);
if stored_captcha.key_type() == KeyType::Empty {
continue;
}
let mut stored_count: usize =
stored_captcha.read().unwrap().unwrap().parse().unwrap();
stored_count -= count;
if stored_count == 0 {
match stored_captcha.delete() {
Err(e) => ctx.log_warning(&format!(
"Error occured while cleaning up captcha when it became 0: {}",
e
)),
Ok(_) => (),
}
} else {
stored_captcha.write(&stored_count.to_string()).unwrap();
}
}
}
None => {
ctx.log_debug(&format!("bucket not found, can't decrement"));
}
}
}
/// executes when timer goes off. Decrements all registered counts and cleans itself up
fn decrement(ctx: &Context, bucket_instant: u64) {
// get bucket
let bucket_name = get_bucket_name(bucket_instant);
let timer = ctx.open_key_writable(&get_timer_name_from_bucket_name(&bucket_name));
let _ = timer.delete();
ctx.log_debug(&format!("Bucket instant: {}", &bucket_instant));
let bucket = ctx.open_key_writable(&bucket_name);
Bucket::decrement_runner(ctx, &bucket);
match bucket.delete() {
Err(e) => ctx.log_warning(&format!("enountered error while deleting hashmap: {:?}", e)),
Ok(_) => (),
}
}
/// Create new counter /// Create new counter
pub fn counter_create(ctx: &Context, args: Vec<String>) -> RedisResult { pub fn counter_create(ctx: &Context, args: Vec<String>) -> RedisResult {
let mut args = args.into_iter().skip(1); let mut args = args.into_iter().skip(1);
@ -270,11 +251,10 @@ pub mod type_methods {
let bucket = match encver { let bucket = match encver {
0 => { 0 => {
let data = raw::load_string(rdb); let data = raw::load_string(rdb);
let fmt = Format::JSON; let bucket: Bucket = Format::JSON.from_str(&data).unwrap();
let bucket: Bucket = fmt.from_str(&data).unwrap();
bucket bucket
} }
_ => panic!("Can't load old RedisJSON RDB"), _ => panic!("Can't load bucket from old redis RDB"),
}; };
// if bucket. // if bucket.

View file

@ -17,13 +17,18 @@
use std::num::ParseIntError; use std::num::ParseIntError;
use derive_more::Display;
use redis_module::RedisError; use redis_module::RedisError;
use redis_module::RedisResult; use redis_module::RedisResult;
#[derive(Debug)] #[derive(Debug, Display)]
pub enum CacheError { pub enum CacheError {
#[display(fmt = "{}", &_0)]
Msg(String), Msg(String),
#[display(fmt = "{}", &_0.to_string)]
RedisError(redis_module::RedisError), RedisError(redis_module::RedisError),
#[display(fmt = "Captcha not found")]
CaptchaNotFound,
} }
impl CacheError { impl CacheError {
@ -50,21 +55,12 @@ impl From<serde_json::Error> for CacheError {
} }
} }
impl From<redis_module::RedisError> for CacheError { impl From<RedisError> for CacheError {
fn from(e: redis_module::RedisError) -> Self { fn from(e: redis_module::RedisError) -> Self {
CacheError::RedisError(e) CacheError::RedisError(e)
} }
} }
impl From<CacheError> for RedisError {
fn from(e: CacheError) -> Self {
match e {
CacheError::Msg(val) => RedisError::String(val),
CacheError::RedisError(val) => val,
}
}
}
impl From<ParseIntError> for CacheError { impl From<ParseIntError> for CacheError {
fn from(e: ParseIntError) -> Self { fn from(e: ParseIntError) -> Self {
let err: RedisError = e.into(); let err: RedisError = e.into();
@ -73,10 +69,17 @@ impl From<ParseIntError> for CacheError {
} }
impl From<CacheError> for RedisResult { impl From<CacheError> for RedisResult {
fn from(e: CacheError) -> Self {
Self::Err(e.into())
}
}
impl From<CacheError> for RedisError {
fn from(e: CacheError) -> Self { fn from(e: CacheError) -> Self {
match e { match e {
CacheError::Msg(val) => Err(RedisError::String(val)), CacheError::Msg(val) => RedisError::String(val),
CacheError::RedisError(val) => Err(val), CacheError::RedisError(val) => val,
CacheError::CaptchaNotFound => RedisError::String(format!("{}", e)),
} }
} }
} }

View file

@ -56,7 +56,7 @@ lazy_static! {
rng.gen() rng.gen()
}; };
/// counter/captcha key prefix /// counter/captcha key prefix
pub static ref PREFIX_COUNTER: String = format!("{}:captcha:{}:", PKG_NAME, *ID); pub static ref PREFIX_CAPTCHA: String = format!("{}:captcha::", PKG_NAME);
/// bucket key prefix /// bucket key prefix
pub static ref PREFIX_BUCKET: String = format!("{}:bucket:{{{}}}:", PKG_NAME, *ID); pub static ref PREFIX_BUCKET: String = format!("{}:bucket:{{{}}}:", PKG_NAME, *ID);
} }
@ -66,8 +66,9 @@ redis_module! {
version: PKG_VERSION, version: PKG_VERSION,
data_types: [MCAPTCHA_BUCKET_TYPE, MCAPTCHA_MCAPTCHA_TYPE], data_types: [MCAPTCHA_BUCKET_TYPE, MCAPTCHA_MCAPTCHA_TYPE],
commands: [ commands: [
["mcaptcha_cache.count", bucket::Bucket::counter_create, "write", 1, 1, 1], ["mcaptcha_cache.add_visitor", bucket::Bucket::counter_create, "write", 1, 1, 1],
["mcaptcha_cache.get", mcaptcha::MCaptcha::get, "readonly", 1, 1, 1], ["mcaptcha_cache.get", mcaptcha::MCaptcha::get_count, "readonly", 1, 1, 1],
["mcaptcha_cache.add_captcha", mcaptcha::MCaptcha::add_captcha, "readonly", 1, 1, 1],
], ],
event_handlers: [ event_handlers: [
[@EXPIRED @EVICTED: bucket::Bucket::on_delete], [@EXPIRED @EVICTED: bucket::Bucket::on_delete],

View file

@ -1,3 +1,4 @@
use redis_module::RedisValue;
/* /*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
* *
@ -14,10 +15,11 @@
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
//use redis_module::key::RedisKeyWritable; use redis_module::key::RedisKeyWritable;
use redis_module::native_types::RedisType; use redis_module::native_types::RedisType;
use redis_module::raw::KeyType; use redis_module::raw::KeyType;
use redis_module::{Context, RedisResult}; use redis_module::{Context, RedisResult};
use redis_module::{NextArg, REDIS_OK};
//use redis_module::RedisError; //use redis_module::RedisError;
use redis_module::raw; use redis_module::raw;
@ -25,9 +27,9 @@ use serde::{Deserialize, Serialize};
use crate::bucket::Format; use crate::bucket::Format;
use crate::errors::*; use crate::errors::*;
use crate::utils; use crate::utils::*;
const REDIS_MCPATCHA_MCAPTCHA_TYPE_VERSION: i32 = 1; const REDIS_MCPATCHA_MCAPTCHA_TYPE_VERSION: i32 = 0;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct MCaptcha { pub struct MCaptcha {
@ -35,6 +37,10 @@ pub struct MCaptcha {
} }
impl MCaptcha { impl MCaptcha {
#[inline]
fn new(m: libmcaptcha::MCaptcha) -> Self {
MCaptcha { m }
}
/// increments the visitor count by one /// increments the visitor count by one
#[inline] #[inline]
pub fn add_visitor(&mut self) { pub fn add_visitor(&mut self) {
@ -53,32 +59,61 @@ impl MCaptcha {
self.m.get_difficulty() self.m.get_difficulty()
} }
/// get [Counter]'s lifetime /// get [MCaptcha]'s lifetime
#[inline] #[inline]
pub fn get_duration(&self) -> u64 { pub fn get_duration(&self) -> u64 {
self.m.get_duration() self.m.get_duration()
} }
/// get [Counter]'s current visitor_threshold /// get [MCaptcha]'s current visitor_threshold
#[inline] #[inline]
pub fn get_visitors(&self) -> u32 { pub fn get_visitors(&self) -> u32 {
self.m.get_visitors() self.m.get_visitors()
} }
/// Get counter value /// decrement [MCaptcha]'s current visitor_threshold by specified count
pub fn get(ctx: &Context, args: Vec<String>) -> RedisResult { #[inline]
use redis_module::NextArg; pub fn decrement_visitor_by(&mut self, count: u32) {
self.m.decrement_visitor_by(count)
}
/// get mcaptcha from redis key writable
pub fn get_mcaptcha<'a>(
ctx: &Context,
key: &'a RedisKeyWritable,
) -> CacheResult<Option<&'a mut Self>> {
Ok(key.get_value::<Self>(&MCAPTCHA_MCAPTCHA_TYPE)?)
}
/// Get counter value
pub fn get_count(ctx: &Context, args: Vec<String>) -> RedisResult {
let mut args = args.into_iter().skip(1); let mut args = args.into_iter().skip(1);
let key_name = args.next_string()?; let key_name = args.next_string()?;
let key_name = utils::get_captcha_key(&key_name); let key_name = get_captcha_key(&key_name);
let stored_captcha = ctx.open_key(&key_name); let stored_captcha = ctx.open_key(&key_name);
if stored_captcha.key_type() == KeyType::Empty { if stored_captcha.key_type() == KeyType::Empty {
return CacheError::new(format!("key {} not found", key_name)).into(); return CacheError::new(format!("key {} not found", key_name)).into();
} }
Ok(stored_captcha.read()?.unwrap().into()) match stored_captcha.get_value::<Self>(&MCAPTCHA_MCAPTCHA_TYPE)? {
Some(val) => Ok(RedisValue::Integer(val.get_visitors().into())),
None => return Err(CacheError::CaptchaNotFound.into()),
}
}
/// Add captcha to redis
pub fn add_captcha(ctx: &Context, args: Vec<String>) -> RedisResult {
let mut args = args.into_iter().skip(1);
let key_name = get_captcha_key(&args.next_string()?);
let json = args.next_string()?;
let mcaptcha: libmcaptcha::MCaptcha = Format::JSON.from_str(&json)?;
let mcaptcha = Self::new(mcaptcha);
let key = ctx.open_key_writable(&&key_name);
key.set_value(&MCAPTCHA_MCAPTCHA_TYPE, mcaptcha)?;
REDIS_OK
} }
} }
@ -120,12 +155,10 @@ pub mod type_methods {
let mcaptcha = match encver { let mcaptcha = match encver {
0 => { 0 => {
let data = raw::load_string(rdb); let data = raw::load_string(rdb);
let mcaptcha: MCaptcha = Format::JSON.from_str(&data).unwrap();
let fmt = Format::JSON;
let mcaptcha: MCaptcha = fmt.from_str(&data).unwrap();
mcaptcha mcaptcha
} }
_ => panic!("Can't load old RedisJSON RDB"), _ => panic!("Can't load mCaptcha from old redis RDB"),
}; };
Box::into_raw(Box::new(mcaptcha)) as *mut c_void Box::into_raw(Box::new(mcaptcha)) as *mut c_void
@ -141,7 +174,7 @@ pub mod type_methods {
let mcaptcha = &*(value as *mut MCaptcha); let mcaptcha = &*(value as *mut MCaptcha);
match &serde_json::to_string(mcaptcha) { match &serde_json::to_string(mcaptcha) {
Ok(string) => raw::save_string(rdb, &string), Ok(string) => raw::save_string(rdb, &string),
Err(e) => eprintln!("error while rdb_save: {}", e), Err(e) => panic!("error while rdb_save: {}", e),
} }
} }
} }

View file

@ -50,7 +50,7 @@ pub fn get_bucket_instant(duration: u64) -> CacheResult<u64> {
#[inline] #[inline]
pub fn get_captcha_key(name: &str) -> String { pub fn get_captcha_key(name: &str) -> String {
format!("{}{}", &*PREFIX_COUNTER, name) format!("{}{}", &*PREFIX_CAPTCHA, name)
} }
#[inline] #[inline]

View file

@ -1,6 +1,5 @@
#!/bin/env python3 #!/bin/env /usr/bin/python3
# # # Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
# Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
@ -15,13 +14,15 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
from time import sleep from time import sleep
import sys
import test; import test
from mcaptcha import register
r = test.r r = test.r
COMMANDS = { COMMANDS = {
"COUNT" : "mcaptcha_cache.count", "COUNT" : "mcaptcha_cache.add_visitor",
"GET" : "mcaptcha_cache.get", "GET" : "mcaptcha_cache.get",
} }
@ -40,27 +41,36 @@ def assert_count(expect, key):
assert count == expect assert count == expect
def incr_one_works(): def incr_one_works():
key = "incr_one" try:
time = 2 key = "incr_one"
initial_count = get_count(key) register(r, key)
# incriment time = 2
incr(key, time) initial_count = get_count(key)
assert_count(initial_count + 1, key) # incriment
# wait till expiry incr(key, time)
sleep(time + 2) assert_count(initial_count + 1, key)
assert_count(initial_count, key) # wait till expiry
print("Incr one works") sleep(time + 2)
assert_count(initial_count, key)
print("Incr one works")
except Exception as e:
raise e
def race_works(): def race_works():
key = "race_works" key = "race_works"
initial_count = get_count(key) try:
race_num = 200 register(r, key)
time = 3 initial_count = get_count(key)
race_num = 200
time = 3
for _ in range(race_num): for _ in range(race_num):
incr(key, time) incr(key, time)
assert_count(initial_count + race_num, key) assert_count(initial_count + race_num, key)
# wait till expiry # wait till expiry
sleep(time + 2) sleep(time + 2)
assert_count(initial_count, key) assert_count(initial_count, key)
print("Race works") print("Race works")
except Exception as e:
raise e

42
tests/mcaptcha.py Normal file
View file

@ -0,0 +1,42 @@
#!/bin/env /usr/bin/python3
#
# Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import json
MCAPTCHA = {
"visitor_threshold": 0,
"defense": {
"levels": [
{"visitor_threshold": 50, "difficulty_factor": 50},
{"visitor_threshold": 500, "difficulty_factor": 500}
],
"current_visitor_threshold": 0
},
"duration": 5
}
COMMANDS = {
"ADD_CAPTCHA": "MCAPTCHA_CACHE.ADD_CAPTCHA",
}
payload = json.dumps(MCAPTCHA)
def register(r, key):
if r.exists(key):
r.delete(key)
r.execute_command(COMMANDS["ADD_CAPTCHA"], key, payload)

View file

@ -1,3 +1,4 @@
#!/bin/env /usr/bin/python3
# Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net> # Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
@ -22,13 +23,24 @@ class Runner(object):
def register(self, fn): def register(self, fn):
self._functions.append(fn) self._functions.append(fn)
t = Thread(target=fn) t = Thread(target=fn)
t.start()
self._threads.append(t) self._threads.append(t)
"""Wait for registered functions to finish executing""" """Wait for registered functions to finish executing"""
def wait(self):
def __run__(self):
for thread in self._threads: for thread in self._threads:
thread.join() try:
thread.start()
except:
print("yo")
def wait(self):
self.__run__()
for thread in self._threads:
try:
thread.join()
except:
print("yo")
"""Runs in seperate threads""" """Runs in seperate threads"""
def __init__(self): def __init__(self):

View file

@ -1,4 +1,4 @@
#!/bin/env python3 #!/bin/env /usr/bin/python3
# #
# Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net> # Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
# #
@ -32,15 +32,20 @@ utils.ping(r)
def main(): def main():
runner = Runner() #runner = Runner()
fn = [bucket.incr_one_works, bucket.race_works] #fn = [bucket.incr_one_works]#, bucket.race_works]
for r in fn:
runner.register(r)
runner.wait() bucket.incr_one_works()
bucket.race_works()
print("All tests passed") #try:
# for r in fn:
# runner.register(r)
# runner.wait()
# print("All tests passed")
#except Exception as e:
# raise e
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View file

@ -1,5 +1,4 @@
#!/bin/env /usr/bin/python3
#!/bin/env python3
# #
# Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net> # Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
# #