import logging import pytest import redis import asyncio from redis import asyncio as aioredis from . import dfly_multi_test_args, dfly_args from .instance import DflyInstance, DflyStartException from .utility import batch_fill_data, gen_test_data, EnvironCntx from .seeder import DebugPopulateSeeder @dfly_multi_test_args({"keys_output_limit": 512}, {"keys_output_limit": 1024}) class TestKeys: async def test_max_keys(self, async_client: aioredis.Redis, df_server): max_keys = df_server["keys_output_limit"] pipe = async_client.pipeline() batch_fill_data(pipe, gen_test_data(max_keys * 3)) await pipe.execute() keys = await async_client.keys() assert len(keys) in range(max_keys, max_keys + 512) @pytest.fixture(scope="function") def export_dfly_password() -> str: pwd = "flypwd" with EnvironCntx(DFLY_requirepass=pwd): yield pwd async def test_password(df_factory, export_dfly_password): with df_factory.create() as dfly: # Expect password form environment variable with pytest.raises(redis.exceptions.AuthenticationError): async with aioredis.Redis(port=dfly.port) as client: await client.ping() async with aioredis.Redis(password=export_dfly_password, port=dfly.port) as client: await client.ping() # --requirepass should take precedence over environment variable requirepass = "requirepass" with df_factory.create(requirepass=requirepass) as dfly: # Expect password form flag with pytest.raises(redis.exceptions.AuthenticationError): async with aioredis.Redis(port=dfly.port, password=export_dfly_password) as client: await client.ping() async with aioredis.Redis(password=requirepass, port=dfly.port) as client: await client.ping() """ Make sure that multi-hop transactions can't run OOO. """ MULTI_HOPS = """ for i = 0, ARGV[1] do redis.call('INCR', KEYS[1]) end """ @dfly_args({"proactor_threads": 1}) async def test_txq_ooo(async_client: aioredis.Redis, df_server): async def task1(k, h): c = aioredis.Redis(port=df_server.port) for _ in range(100): await c.eval(MULTI_HOPS, 1, k, h) async def task2(k, n): c = aioredis.Redis(port=df_server.port) for _ in range(100): pipe = c.pipeline(transaction=False) pipe.lpush(k, 1) for _ in range(n): pipe.blpop(k, 0.001) await pipe.execute() await asyncio.gather( task1("i1", 2), task1("i2", 3), task2("l1", 2), task2("l1", 2), task2("l1", 5) ) @dfly_args({"proactor_threads": 2, "num_shards": 2}) async def test_blocking_multiple_dbs(async_client: aioredis.Redis, df_server: DflyInstance): active = True # A task to trigger the flow that eventually looses a transaction # blmove is used to trigger a global deadlock, but we could use any # command - the effect would be - a deadlocking locally that connection async def blmove_task_loose(num): async def run(id): c = df_server.client() await c.lpush(f"key{id}", "val") while active: await c.blmove(f"key{id}", f"key{id}", 0, "LEFT", "LEFT") await asyncio.sleep(0.01) tasks = [] for i in range(num): tasks.append(run(i)) await asyncio.gather(*tasks) # A task that creates continuation_trans_ by constantly timing out on # an empty set. We could probably use any 2-hop operation like rename. async def task_blocking(num): async def block(id): c = df_server.client() while active: await c.blmove(f"{{{id}}}from", f"{{{id}}}to", 0.1, "LEFT", "LEFT") tasks = [] for i in range(num): tasks.append(block(i)) await asyncio.gather(*tasks) # produce is constantly waking up consumers. It is used to trigger the # flow that creates wake ups on a differrent database in the # middle of continuation transaction. async def tasks_produce(num, iters): LPUSH_SCRIPT = """ redis.call('LPUSH', KEYS[1], "val") """ async def produce(id): c = df_server.client(db=1) # important to be on a different db for i in range(iters): # Must be a lua script and not multi-exec for some reason. await c.eval(LPUSH_SCRIPT, 1, f"list{{{id}}}") tasks = [] for i in range(num): task = asyncio.create_task(produce(i)) tasks.append(task) await asyncio.gather(*tasks) logging.info("Finished producing") # works with producer to constantly block and wake up async def tasks_consume(num, iters): async def drain(id, iters): client = df_server.client(db=1) for _ in range(iters): await client.blmove(f"list{{{id}}}", f"sink{{{id}}}", 0, "LEFT", "LEFT") tasks = [] for i in range(num): task = asyncio.create_task(drain(i, iters)) tasks.append(task) await asyncio.gather(*tasks) logging.info("Finished consuming") num_keys = 32 num_iters = 200 async_task1 = asyncio.create_task(blmove_task_loose(num_keys)) async_task2 = asyncio.create_task(task_blocking(num_keys)) logging.info("Starting tasks") await asyncio.gather( tasks_consume(num_keys, num_iters), tasks_produce(num_keys, num_iters), ) logging.info("Finishing tasks") active = False await asyncio.gather(async_task1, async_task2) async def test_arg_from_environ_overwritten_by_cli(df_factory): with EnvironCntx(DFLY_port="6378"): with df_factory.create(port=6377): client = aioredis.Redis(port=6377) await client.ping() async def test_arg_from_environ(df_factory): with EnvironCntx(DFLY_requirepass="pass"): with df_factory.create() as dfly: # Expect password from environment variable with pytest.raises(redis.exceptions.AuthenticationError): client = aioredis.Redis(port=dfly.port) await client.ping() client = aioredis.Redis(password="pass", port=dfly.port) await client.ping() async def test_unknown_dfly_env(df_factory, export_dfly_password): with EnvironCntx(DFLY_abcdef="xyz"): dfly = df_factory.create() with pytest.raises(DflyStartException): dfly.start() dfly.set_proc_to_none() async def test_restricted_commands(df_factory): # Restrict GET and SET, then verify non-admin clients are blocked from # using these commands, though admin clients can use them. with df_factory.create(restricted_commands="get,set", admin_port=1112) as server: async with aioredis.Redis(port=server.port) as client: with pytest.raises(redis.exceptions.ResponseError): await client.get("foo") with pytest.raises(redis.exceptions.ResponseError): await client.set("foo", "bar") async with aioredis.Redis(port=server.admin_port) as admin_client: await admin_client.get("foo") await admin_client.set("foo", "bar") @pytest.mark.asyncio async def test_reply_guard_oom(df_factory, df_seeder_factory): master = df_factory.create( proactor_threads=1, cache_mode="true", maxmemory="256mb", enable_heartbeat_eviction="false", rss_oom_deny_ratio=2, ) df_factory.start_all([master]) c_master = master.client() await c_master.execute_command("DEBUG POPULATE 6000 size 40000") seeder = df_seeder_factory.create( port=master.port, keys=5000, val_size=1000, stop_on_failure=False ) await seeder.run(target_deviation=0.1) info = await c_master.info("stats") assert info["evicted_keys"] > 0, "Weak testcase: policy based eviction was not triggered." @pytest.mark.asyncio async def test_denyoom_commands(df_factory): df_server = df_factory.create(proactor_threads=1, maxmemory="256mb", oom_deny_commands="get") df_server.start() client = df_server.client() await client.execute_command("DEBUG POPULATE 7000 size 44000") min_deny = 256 * 1024 * 1024 # 256mb info = await client.info("memory") print(f'Used memory {info["used_memory"]}, rss {info["used_memory_rss"]}') assert info["used_memory"] > min_deny, "Weak testcase: too little used memory" # reject set due to oom with pytest.raises(redis.exceptions.ResponseError): await client.execute_command("set x y") # reject get because it is set in oom_deny_commands with pytest.raises(redis.exceptions.ResponseError): await client.execute_command("get x") # mget should not be rejected await client.execute_command("mget x") @pytest.mark.parametrize("type", ["LIST", "HASH", "SET", "ZSET", "STRING"]) @dfly_args({"proactor_threads": 4}) @pytest.mark.asyncio async def test_rename_huge_values(df_factory, type): df_server = df_factory.create() df_server.start() client = df_server.client() logging.debug(f"Generating huge {type}") seeder = DebugPopulateSeeder( key_target=1, data_size=10_000_000, collection_size=10_000, variance=1, samples=1, types=[type], ) await seeder.run(client) source_data = await DebugPopulateSeeder.capture(client) logging.debug(f"src {source_data}") # Rename multiple times to make sure the key moves between shards orig_name = (await client.execute_command("keys *"))[0] old_name = orig_name new_name = "" for i in range(10): new_name = f"new:{i}" await client.execute_command(f"rename {old_name} {new_name}") old_name = new_name await client.execute_command(f"rename {new_name} {orig_name}") target_data = await DebugPopulateSeeder.capture(client) assert source_data == target_data @pytest.mark.asyncio async def test_key_bump_ups(df_factory): master = df_factory.create( proactor_threads=2, cache_mode="true", ) df_factory.start_all([master]) c_master = master.client() await c_master.execute_command("DEBUG POPULATE 18000 KEY 32 RAND") info = await c_master.info("stats") assert info["bump_ups"] == 0 keys = await c_master.execute_command("SCAN 0") keys = keys[1][0:10] # Bump keys for key in keys: await c_master.execute_command("GET " + key) info = await c_master.info("stats") assert info["bump_ups"] <= 10 # Multi get bump await c_master.execute_command("MGET " + " ".join(keys)) info = await c_master.info("stats") assert info["bump_ups"] >= 10 and info["bump_ups"] <= 20 last_bump_ups = info["bump_ups"] for key in keys: await c_master.execute_command("DEL " + key) # DEL should not bump up any key info = await c_master.info("stats") assert last_bump_ups == info["bump_ups"] # Find key that has slot > 0 and bump it while True: keys = await c_master.execute_command("SCAN 0") key = keys[1][0] debug_key_info = await c_master.execute_command("DEBUG OBJECT " + key) slot_id = int(dict(map(lambda s: s.split(":"), debug_key_info.split()))["slot"]) if slot_id == 0: # delete the key and continue await c_master.execute_command("DEL " + key) continue await c_master.execute_command("GET " + key) debug_key_info = await c_master.execute_command("DEBUG OBJECT " + key) new_slot_id = int(dict(map(lambda s: s.split(":"), debug_key_info.split()))["slot"]) assert new_slot_id + 1 == slot_id break