Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 8 additions & 10 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ members = [
]

[workspace.dependencies]
ijson = { git="https://github.com/RedisJSON/ijson", rev="e0119ac74f6c4ee918718ee122c3948b74ebeba8", default_features=false}
ijson = { git="https://github.com/RedisJSON/ijson", rev="eede48fad51b4ace5043d3e0714f5a65481a065d", default_features=false}
serde_json = { version="1", features = ["unbounded_depth"]}
serde = { version = "1", features = ["derive"] }
serde_derive = "1"
Expand Down
5 changes: 3 additions & 2 deletions redis_json/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@ ijson.workspace = true
serde_json.workspace = true
serde.workspace = true
libc = "0.2"
redis-module ={ version = "^2.0.7", default-features = false, features = ["min-redis-compatibility-version-7-2"] }
redis-module-macros = "^2.0.7"
redis-module ={ git="https://github.com/RedisLabsModules/redismodule-rs", rev="32f284bd18ee799e54f8da835c81a137a34d7f83", default-features = false, features = ["min-redis-compatibility-version-7-2"] }
redis-module-macros = { git="https://github.com/RedisLabsModules/redismodule-rs", rev="32f284bd18ee799e54f8da835c81a137a34d7f83" }
itertools = "0.13"
json_path = {path="../json_path"}
linkme = "0.3"
lazy_static = "1"

[features]
as-library = []
Expand Down
2 changes: 2 additions & 0 deletions redis_json/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* the Server Side Public License v1 (SSPLv1).
*/

use crate::defrag::defrag_info;
use crate::error::Error;
use crate::formatter::ReplyFormatOptions;
use crate::key_value::KeyValue;
Expand Down Expand Up @@ -1817,6 +1818,7 @@ pub fn json_debug<M: Manager>(manager: M, ctx: &Context, args: Vec<RedisString>)
.into())
}
}
"DEFRAG_INFO" => defrag_info(ctx),
"HELP" => {
let results = vec![
"MEMORY <key> [path] - reports memory usage",
Expand Down
106 changes: 106 additions & 0 deletions redis_json/src/defrag.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
use std::{
alloc::Layout,
os::raw::{c_int, c_void},
};

use ijson::{Defrag, DefragAllocator};
use lazy_static::lazy_static;
use redis_module::{
defrag::DefragContext, raw, redisvalue::RedisValueKey, Context, RedisGILGuard, RedisResult,
RedisValue,
};
use redis_module_macros::{defrag_end_function, defrag_start_function};

use crate::redisjson::RedisJSON;

#[derive(Default)]
pub(crate) struct DefragStats {
defrag_started: usize,
defrag_ended: usize,
keys_defrag: usize,
}

lazy_static! {
pub(crate) static ref DEFRAG_STATS: RedisGILGuard<DefragStats> = RedisGILGuard::default();
}

struct DefragCtxAllocator<'dc> {
defrag_ctx: &'dc DefragContext,
}

impl<'dc> DefragAllocator for DefragCtxAllocator<'dc> {
unsafe fn realloc_ptr<T>(&mut self, ptr: *mut T, _layout: Layout) -> *mut T {
self.defrag_ctx.defrag_realloc(ptr)
}

/// Allocate memory for defrag
unsafe fn alloc(&mut self, layout: Layout) -> *mut u8 {
self.defrag_ctx.defrag_alloc(layout)
}

Check warning on line 39 in redis_json/src/defrag.rs

View check run for this annotation

Codecov / codecov/patch

redis_json/src/defrag.rs#L37-L39

Added lines #L37 - L39 were not covered by tests

/// Free memory for defrag
unsafe fn free<T>(&mut self, ptr: *mut T, layout: Layout) {
self.defrag_ctx.defrag_dealloc(ptr, layout)
}

Check warning on line 44 in redis_json/src/defrag.rs

View check run for this annotation

Codecov / codecov/patch

redis_json/src/defrag.rs#L42-L44

Added lines #L42 - L44 were not covered by tests
}

#[defrag_start_function]
fn defrag_start(defrag_ctx: &DefragContext) {
let mut defrag_stats = DEFRAG_STATS.lock(defrag_ctx);
defrag_stats.defrag_started += 1;
ijson::reinit_shared_string_cache();
}

Check warning on line 52 in redis_json/src/defrag.rs

View check run for this annotation

Codecov / codecov/patch

redis_json/src/defrag.rs#L47-L52

Added lines #L47 - L52 were not covered by tests

#[defrag_end_function]
fn defrag_end(defrag_ctx: &DefragContext) {
let mut defrag_stats = DEFRAG_STATS.lock(defrag_ctx);
defrag_stats.defrag_ended += 1;
}

Check warning on line 58 in redis_json/src/defrag.rs

View check run for this annotation

Codecov / codecov/patch

redis_json/src/defrag.rs#L54-L58

Added lines #L54 - L58 were not covered by tests

#[allow(non_snake_case, unused)]
pub unsafe extern "C" fn defrag(
ctx: *mut raw::RedisModuleDefragCtx,
key: *mut raw::RedisModuleString,
value: *mut *mut c_void,
) -> c_int {
let defrag_ctx = DefragContext::new(ctx);

let mut defrag_stats = DEFRAG_STATS.lock(&defrag_ctx);
defrag_stats.keys_defrag += 1;

let mut defrag_allocator = DefragCtxAllocator {
defrag_ctx: &defrag_ctx,
};
let value = value.cast::<*mut RedisJSON<ijson::IValue>>();
let new_val = defrag_allocator.realloc_ptr(*value, Layout::new::<RedisJSON<ijson::IValue>>());
if !new_val.is_null() {
*value = new_val;
}
std::ptr::write(
&mut (**value).data as *mut ijson::IValue,
std::ptr::read(*value).data.defrag(&mut defrag_allocator),
);
0
}

pub(crate) fn defrag_info(ctx: &Context) -> RedisResult {
let defrag_stats = DEFRAG_STATS.lock(ctx);
Ok(RedisValue::OrderedMap(
[
(
RedisValueKey::String("defrag_started".to_owned()),
RedisValue::Integer(defrag_stats.defrag_started as i64),
),
(
RedisValueKey::String("defrag_ended".to_owned()),
RedisValue::Integer(defrag_stats.defrag_ended as i64),
),
(
RedisValueKey::String("keys_defrag".to_owned()),
RedisValue::Integer(defrag_stats.keys_defrag as i64),
),
]
.into_iter()
.collect(),
))
}
3 changes: 2 additions & 1 deletion redis_json/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ mod array_index;
mod backward;
pub mod c_api;
pub mod commands;
pub mod defrag;
pub mod error;
mod formatter;
pub mod ivalue_manager;
Expand Down Expand Up @@ -73,7 +74,7 @@ pub static REDIS_JSON_TYPE: RedisType = RedisType::new(
free_effort: None,
unlink: None,
copy: Some(redisjson::type_methods::copy),
defrag: None,
defrag: Some(defrag::defrag),

free_effort2: None,
unlink2: None,
Expand Down
83 changes: 83 additions & 0 deletions tests/pytest/test_defrag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import time
import json
from RLTest import Defaults

Defaults.decode_responses = True

def enableDefrag(env):
# make defrag as aggressive as possible
env.cmd('CONFIG', 'SET', 'hz', '100')
env.cmd('CONFIG', 'SET', 'active-defrag-ignore-bytes', '1')
env.cmd('CONFIG', 'SET', 'active-defrag-threshold-lower', '0')
env.cmd('CONFIG', 'SET', 'active-defrag-cycle-min', '99')

try:
env.cmd('CONFIG', 'SET', 'activedefrag', 'yes')
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MeirShpilraien i understand that unlike the redis defrag tests, this one isn't actually creating fragmentation and waiting for it to go down, which is ok.
and i understand that it does test all sorts of various types of json objects, and watches for stats about callback cycles, but how does it make sure that the pointers were really moved? what if redis forgets to call the module api callback?

maybe in addition to these, add one test that create holes and waits for them to get filled?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modify to tests to actually cause fragmentation and verify that defrag actually took place.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if i read it correctly, you created a lot of jason fields, and then deleted all of them.
what i think you should do:

  1. disable active defrag
  2. create a lot of jason fields
  3. delete all the even ones (or alternatively when you create them, you can create two keys, and interchangeably add fields to both, and then delete one key)
  4. verify you managed to create fragmentation beyond a certain threshold.
  5. enable active defrag.
  6. wait for fragmentation levels to go down

see memefficiency.tcl in redis.
what i wanna see is that fields get actually re-located (not just that the callback is called)

except Exception:
# If active defrag is not supported by the current Redis, simply skip the test.
env.skip()

def defragOnObj(env, obj):
enableDefrag(env)
env.expect('JSON.SET', 'test', '$', json.dumps(obj)).ok()
_, _, _, _, _, keysDefrag = env.cmd('JSON.DEBUG', 'DEFRAG_INFO')
startTime = time.time()
# Wait for at least 2 defrag full cycles
# We verify only the 'keysDefrag' value because the other values
# are not promised to be updated. It depends if Redis support
# the start/end defrag callbacks.
while keysDefrag < 2:
time.sleep(0.1)
_, _, _, _, _, keysDefrag = env.cmd('JSON.DEBUG', 'DEFRAG_INFO')
if time.time() - startTime > 30:
# We will wait for up to 30 seconds and then we consider it a failure
env.assertTrue(False, message='Failed waiting for defrag to run')
return
# make sure json is still valid.
res = json.loads(env.cmd('JSON.GET', 'test', '$'))[0]
env.assertEqual(res, obj)

def testDefragNumber(env):
defragOnObj(env, 1)

def testDefragBigNumber(env):
defragOnObj(env, 100000000000000000000)

def testDefragDouble(env):
defragOnObj(env, 1.111111111111)

def testDefragNegativeNumber(env):
defragOnObj(env, -100000000000000000000)

def testDefragNegativeDouble(env):
defragOnObj(env, -1.111111111111)

def testDefragTrue(env):
defragOnObj(env, True)

def testDefragFalse(env):
defragOnObj(env, True)

def testDefragNone(env):
defragOnObj(env, None)

def testDefragEmptyString(env):
defragOnObj(env, "")

def testDefragString(env):
defragOnObj(env, "foo")

def testDefragEmptyArray(env):
defragOnObj(env, [])

def testDefragArray(env):
defragOnObj(env, [1, 2, 3])

def testDefragEmptyObject(env):
defragOnObj(env, {})

def testDefragObject(env):
defragOnObj(env, {"foo": "bar"})

def testDefragComplex(env):
defragOnObj(env, {"foo": ["foo", 1, None, True, False, {}, {"foo": [], "bar": 1}]})
Loading