Skip to content

Add ReuseServerTestCase for class-scoped server reuse - #13

Open
Fniakate8 wants to merge 9 commits into
valkey-io:unstablefrom
Fniakate8:server-reuse-upstream
Open

Add ReuseServerTestCase for class-scoped server reuse#13
Fniakate8 wants to merge 9 commits into
valkey-io:unstablefrom
Fniakate8:server-reuse-upstream

Conversation

@Fniakate8

@Fniakate8 Fniakate8 commented Jul 29, 2026

Copy link
Copy Markdown

This PR implements server reuse for test classes, keeping one server alive across all tests in a class instead of spawning and killing a new process per test. Between tests the server is reset to a clean state so each test still starts as isolated.

What's Added

- ReuseServerTestCase inherits ValkeyTestCase and overrides create_server() to cache the server on the first call — subsequent calls return the cached instance

  • Configs are snapshotted on server creation and any changed values are restored between tests to prevent pollution
  • Between tests, _reset_server_state() restores full isolation:
    • RESET on the shared connection (discards MULTI/WATCH, disables CLIENT TRACKING, restores RESP2/db0/READWRITE, etc.)
    • CLIENT KILL TYPE normal to drop any connections a test spawned (SKIPME keeps the shared client alive)
    • REPLICAOF NO ONE, FLUSHALL, CONFIG RESETSTAT, SCRIPT FLUSH, FUNCTION FLUSH
    • SLOWLOG RESET, LATENCY RESET, ACL LOG RESET so per-test log checks start clean
    • ACL reset (delete non-default users, reset default user to full permissions with nopass)
    • Config restore against the initial snapshot
  • If the server becomes unreachable or a config can't be restored, it's torn down and a fresh one starts for the next test
  • Additional servers created during a test are tracked in server_list and cleaned up (the shared server is skipped)

How to use

To adopt server reuse in your module:

  1. Import ReuseServerTestCase from the framework
  2. Change your test base class to inherit from ReuseServerTestCase instead of ValkeyTestCase

Your existing setup_test, create_server() calls, and self.server/self.client assignments all work unchanged. Tests that need a fresh server (restart the server, change startup-only configs, do persistence operations)
should stay on ValkeyTestCase.

How I tested with other modules

  • Bloom module — Changed ValkeyBloomTestCaseBase to inherit ReuseServerTestCase. Ran test_bloom_basic.py (70 tests). All 70 passed. Tests that use CONFIG SET (like bf.bloom-memory-usage-limit, maxmemory) no longer
    pollute later tests thanks to the automatic config restore.
  • JSON module — Changed JsonTestCase to inherit ReuseServerTestCase. Ran test_json_basic.py (171 tests). 170 passed, 1 failed due to a pre-existing missing SOURCE_DIR env var (unrelated to server reuse).

Server reuse helps most when module loading is expensive (like JSON) and tests are lightweight data operations. Bloom sees less improvement because its tests are compute-heavy (filling filters, sleeping for
expiration).

The framework's own tests/test_reuse_server.py also verifies the reset behavior directly: data isolation, config restore, and that a client connection + ACL user left behind in one test are gone in the next.

Test plan

  • Bloom: all 34 tests pass with only base class change
  • JSON: 170/171 pass with only base class change
  • Config-heavy tests no longer pollute subsequent tests
  • Connection state, spawned clients, ACL users, and logs are reset between tests
  • Tests that need a fresh server can stay on ValkeyTestCase

@Fniakate8
Fniakate8 force-pushed the server-reuse-upstream branch 2 times, most recently from e94d1d4 to 5006368 Compare July 29, 2026 01:14
Fanta Niakate added 2 commits July 30, 2026 17:45
Instead of spawning a fresh valkey-server per test (~42ms), one server
is started per class and FLUSHALL + CONFIG RESETSTAT (~5ms) run between
tests to reset state. This gives ~5-7x speedup for test classes.

Changes:
- src/conftest.py: add class_port_tracker fixture (scope="class")
- src/valkey_test_case.py: add ReuseServerTestCase class
- tests/test_reuse_server.py: demo/test proving reuse and isolation

Signed-off-by: Fanta Niakate <niakatf@amazon.com>
ReuseServerTestCase now inherits ValkeyTestCase and overrides
create_server() to cache the server on first call. Modules only need to
change their base class — no other code modifications required.

Key changes:
- Inherit ValkeyTestCase instead of ValkeyTestCaseBase so all fixtures
  (setup, port_tracker_fixture) work automatically
- Override create_server() to return cached server on subsequent calls
- Snapshot all configs on first creation and restore between tests to
  prevent config pollution across tests
- FLUSHALL + CONFIG RESETSTAT between tests for data isolation
- Bump pytest to 7.4.3

Tested against bloom (34/34 pass) and JSON (170/171 pass, 1 unrelated
env var issue). JSON sees ~17x speedup (8s vs 138s).

Signed-off-by: Fanta Niakate <niakatf@amazon.com>
@Fniakate8
Fniakate8 force-pushed the server-reuse-upstream branch from de8c642 to 88d0f96 Compare July 30, 2026 17:50

@chinguyen21 chinguyen21 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Overall looks better a lot after the 2nd change. Just a few things to improve. Please check the comments



class TestReuseServer(ReuseServerTestCase):
"""Verifies that server reuse works and tests are isolated."""

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Lack of setup_test to call create_server() after redesigning the ReuseServerTestCase in the 2nd commit

@Fniakate8 Fniakate8 Jul 30, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

you're correct. After the redesign of the server reuse, the server is only created when create_server() is first called. I'll add a setup_test fixture taht calls create_server().

Comment thread requirements.txt Outdated
@@ -1,4 +1,4 @@
valkey
pytest==6
pytest==7.4.3

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why do we need this change? Doesn't pytest6 work?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The pytest 6 works but it throws deprecation warnings for the teardown(self) method and pytest 7 handles fixture scoping more reliably (like our class-scoped class_teardown fixture.) but for simplicity I'll remove it.

Comment thread src/valkey_test_case.py
def teardown(self):
if hasattr(self.__class__, '_shared_server') and self.__class__._shared_server:
client = self.__class__._shared_client
client.flushall()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If the server somehow crashes during a test, this will raise an exception. Can we handle it? And make sure if exception happens, we should discard the shared server, so other tests can still run properly

@Fniakate8 Fniakate8 Jul 30, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. I wrapped the teardown in a try/except. If the server crashes, we can just discard _shared_server and _shared_client by setting them to None. The next test's create_server will see None and spin up a new server for concurrent tests.

Comment thread src/valkey_test_case.py

def create_server(
self,
testdir=None,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is there any benefit to not require testdir?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added a fallback: if testdir isn't passed, it defaults to self.testdir (same approach as server_path). This way None never reaches the parent, and modules that already pass testdir=self.testdir still work the same.

@Fniakate8
Fniakate8 force-pushed the server-reuse-upstream branch from 482ecaa to 53af29f Compare July 30, 2026 23:59
@zackcam

zackcam commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

I let workflows run seems like there are some formatting issues can run black . to fix formatting issues.

Comment thread src/conftest.py Outdated


@pytest.fixture(scope="class")
def class_port_tracker(request):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do we need this still? I saw we had an import for this but removed it

@Fniakate8
Fniakate8 force-pushed the server-reuse-upstream branch from 53af29f to c3685d4 Compare July 31, 2026 21:06
- Handle server crash in teardown: if flushall/config reset fails,
  discard the shared server so next test creates a fresh one
- Default testdir to self.testdir when not passed (same pattern as
  server_path) so None never reaches the parent
- Add setup_test fixture to test_reuse_server.py to call create_server()
  matching the pattern modules use
- Remove unused class_port_tracker import from test file
- Revert pytest version bump — not needed for server reuse

Signed-off-by: Fanta Niakate <niakatf@amazon.com>
@Fniakate8
Fniakate8 force-pushed the server-reuse-upstream branch from 1ecb724 to eb44776 Compare August 3, 2026 23:32

@zackcam zackcam left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looking good! Still seems to have workflow failures with formatting hopefully should be easy to fix.
Do we also want to reset ACL? Script and function also might persist through? Potentially replication as well i.e made the server a replica should we check if we want to reset that?

from valkey_test_case import ReuseServerTestCase


class TestReuseServer(ReuseServerTestCase):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

For the tests I think they should run top to bottom? Should we make a note that this is how the ordering works somewhere?

@Fniakate8 Fniakate8 Aug 3, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The tests already run top to bottom. I'll reference it explicitly in the README file and py file



class TestReuseServer(ReuseServerTestCase):
"""Verifies that server reuse works and tests are isolated."""

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we add a test for resetting a config?

Comment thread src/valkey_test_case.py Outdated
for key, val in self.__class__._initial_config.items():
if current.get(key) != val:
try:
client.config_set(key, val)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

As we catch here we silently could not reset a config. I think we should at least put a comment if this happens, or even this time actually tear down the server fully and restart one

Comment thread src/valkey_test_case.py

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We should look at updating readme for this as well to show this new functionality

Comment thread src/valkey_test_case.py
client.config_set(key, val)
except Exception:
pass
except Exception:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could this potentially leave behind process that won't be cleaned up?

Fanta Niakate added 3 commits August 4, 2026 00:04
- Add ReuseServerTestCase usage section to README
- Add note about test ordering (top-to-bottom via pytest-order)
- Log warning and tear down server when config_set fails to restore
- Call exit() on server before discarding when teardown hits an exception
- Add test_config_change_is_restored and test_config_restored_after_previous

Signed-off-by: Fanta Niakate <niakatf@amazon.com>
Signed-off-by: Fanta Niakate <niakatf@amazon.com>
…down

ReuseServerTestCase.teardown() now also resets:
- REPLICAOF NO ONE (restore to primary if made replica)
- SCRIPT FLUSH (clear cached Lua scripts)
- FUNCTION FLUSH (clear loaded functions, no-op on Valkey < 7)
- ACL DELUSER for non-default users + reset default user permissions

Also adds explicit `import logging` instead of relying on wildcard import.

Signed-off-by: Fanta Niakate <niakatf@amazon.com>
Comment thread README.md
assert self.client.execute_command("GET K") is None
```

`ReuseServerTestCase` inherits `ValkeyTestCase`, so all existing fixtures, `create_server()` calls, and `self.server`/`self.client` assignments work unchanged. To adopt it in your module, just change the base class — no other code changes needed.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think the create_server() might do some funky things, do we tear it down properly as we override the teardown list? I think we also might return early here and not actually create a new server

@chinguyen21 chinguyen21 Aug 4, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think it should be fine. We don't have a teardown list as we will reuse the _shared_server unless it crashes somehow in previous test. We properly tear down the existing server in class_teardown. Though it's good point that our override teardown function is mainly doing the reset config.
@Fniakate8 We properly can add comment like Reset shared server state between tests instead of shutting it down. at the beginning of your teardown function, and properly creating a private method _reset_server_state and call it inside teardown.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I was thinking more on the lines if we call create server in a test then we will need to tear down the extra one we created.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The server cannot create a extra test, it just resets its configs, and if it can't reset it, it logs a warning, kills the server and creates a new one . But I just Extracted it into _reset_server_state() to make the intent more clear at a glance. ;)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

In that case the readme isn't quite right then, as the create_server would not return a new server if called in a test. If we are adding it to be the same we should allow the functionality to allow the user to call create server in a test and have it return a new server and teardown at the end of that test

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Discussed offline with @zackcam.
@Fniakate8 There are some tests in bloom and search modules which call create_server inside the test itself, then we might not properly clean them up. We should properly clean up all the servers in the list in class_teardown similar to the current teardown.

Comment thread tests/test_reuse_server.py Outdated
def test_config_restored_after_previous(self):
"""Proves the config changed in the previous test was reset."""
current = self.client.config_get("hz")["hz"]
assert current == "10", f"Expected hz=10 (default), got hz={current}"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Not against hardcoding this but if its easy might be better to do where we capture the value of the config at startup but again small nit this shouldnt change i think

Comment thread src/valkey_test_case.py Outdated
username = entry.split(" ")[1]
client.execute_command("ACL", "DELUSER", username)
client.execute_command(
"ACL", "SETUSER", "default", "reset", "on", "~*", "&*", "+@all"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Default user should also not need a password from my understanding

Comment thread tests/test_reuse_server.py Outdated

@pytest.fixture(autouse=True)
def setup_test(self, setup):
server_path = f"{os.path.dirname(os.path.realpath(__file__))}/.build/binaries/{os.environ['SERVER_VERSION']}/valkey-server"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

For this if someone wants to run this without using build.sh we should default to checking unstable if they haven't set server version

Fanta Niakate added 2 commits August 4, 2026 19:45
…llback

- Add nopass to ACL SETUSER default reset to ensure no password required
- Extract _reset_server_state() from teardown for clarity
- Add comment on create_server() explaining cached return behavior
- Capture initial hz value in test instead of hardcoding
- Fall back to "unstable" if SERVER_VERSION env var not set
- Clarify README on caching and reset behavior

Signed-off-by: Fanta Niakate <niakatf@amazon.com>
- Clean up additional servers in teardown() (skip shared server)
- Add safety-net server_list cleanup in class_teardown with getattr
- Add docstring to _reset_server_state explaining reset behavior
- Update class docstring to accurately list all reset operations
- Document additional server cleanup in README

Signed-off-by: Fanta Niakate <niakatf@amazon.com>
@Fniakate8
Fniakate8 force-pushed the server-reuse-upstream branch from db2c9b8 to 6e9aeaa Compare August 4, 2026 23:57

@chinguyen21 chinguyen21 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM

@zackcam

zackcam commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Sorry for churn found a couple more places we should reset which I missed before :( but code looks good to me now.

  1. SLOWLOG RESET, LATENCY RESET, ACL LOG RESET should also be run to reset in case of log checks
  2. We have a shared client so should issue a reset for it, this will do all these: https://valkey.io/commands/reset/
  3. If we spawn a new client in a test should we kill it? can maybe issue client kills for everything but the shared one

Addresses mentor feedback on additional state to reset between tests:
- RESET the shared connection first (discards MULTI/WATCH, disables CLIENT
  TRACKING, restores RESP2/db0/READWRITE, etc.) so later commands aren't
  queued inside a lingering MULTI
- CLIENT KILL TYPE normal to drop any connections a test spawned
  (SKIPME yes default keeps the shared client alive)
- SLOWLOG RESET, LATENCY RESET, ACL LOG RESET so per-test log checks
  start clean

Add tests proving a spawned connection and an ACL user left behind in one
test are gone in the next, and sync the class/method/README/test docstrings
with the full set of reset operations.

Signed-off-by: Fanta Niakate <niakatf@amazon.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants