This project implements a simple Redis-like cache server that supports basic commands for key-value storage and retrieval. The server allows clients to connect and interact with the cache using a custom protocol. It supports commands such as SET, GET, DEL, INCR, DECR, LPUSH, RPUSH, LRANGE, SAVE, PING, ECHO, and CONFIG.
The project has the following directory structure:
.
├── README.md
├── command_handler
│ ├── init.py
│ ├── handler.py
│ ├── parser.py
│ ├── serializer.py
│ └── utils.py
├── file_store_127.0.0.1.json
├── main.py
└── storage
├── init.py
└── cache.py
-
main.py: Entry point for the cache server. It sets up a server to handle client connections and delegates command handling to theCommandHandler. -
command_handlerDirectory: Contains modules for handling commands, parsing commands, serializing responses, and utility functions. -
storageDirectory: Manages the cache and file storage.
-
Make sure you have Python installed on your system.
-
Install the required dependencies:
pip install asyncio
Run the cache server:
python main.pyThe server will start listening on localhost:6379.
Usage Connect to the server using a Redis client or a tool like redis-cli and send commands using the custom protocol.
Example:
$ redis-cli -h localhost -p 6379
localhost:6379> SET mykey myvalue
+OK
localhost:6379> GET mykey
$5
myvalueSupported Commands
SET: Set the value of a key.
GET: Get the value of a key.
DEL: Delete one or more keys.
INCR: Increment the integer value of a key.
DECR: Decrement the integer value of a key.
LPUSH: Insert elements at the beginning of a list.
RPUSH: Insert elements at the end of a list.
LRANGE: Get a range of elements from a list.
SAVE: Save the cache to the file system.
PING: Ping the server.
ECHO: Echo the input.
Refer to the source code in command_handler/handler.py for a complete list of supported commands.
The main.py file serves as the entry point for the Redis-like cache server. It orchestrates the setup of the server, handles incoming client connections, and delegates the processing of commands to the CommandHandler.
- File:
main.py - Responsibility: Entry point for the Redis-like cache server.
- Dependencies: Requires the
asynciomodule and other components from the project.
-
Server Initialization: The script initializes the cache server using the
asyncio.start_serverfunction, specifying the host (localhost) and port (6379). It also sets up a callback function (handle_client) to handle incoming client connections. -
Client Connection Handling: For each incoming client connection, the
handle_clientfunction is invoked. It extracts the client's IP address, creates a newCommandParserinstance, and acquires the correspondingCacheinstance using theCacheHolder. It then enters a loop to continuously read and handle client commands. -
Command Processing: The
CommandParseris responsible for parsing the incoming commands from clients. The parsed commands are then passed to theCommandHandlerfor execution. TheCommandHandlerprocesses the commands and returns the appropriate responses. -
Response Handling: The server sends the responses back to the connected clients after processing each command. The responses are encoded as UTF-8 and sent using the
writer.writemethod. -
Server Start: Finally, the script calls
asyncio.run(main())to start the server and listen for incoming client connections indefinitely.
-
Server Startup: Execute the
main.pyscript to start the Redis-like cache server.python main.py
The handler.py file contains the CommandHandler class, which is responsible for processing Redis-like commands received by the cache server. It interprets and executes these commands, interacting with the cache and providing appropriate responses to clients.
- File:
handler.py - Responsibility: Command handling and interaction with the cache.
- Dependencies: Utilizes components from the project, including the
Serializer,SerializorFactory, and the cache (RedisCache).
-
Command Handling: The
CommandHandlerclass interprets Redis-like commands and executes corresponding actions on the cache. It supports a variety of commands such asSET,GET,INCR,DECR,LPUSH,RPUSH,LRANGE, and others. -
Command Mapping: The class maintains a mapping of supported commands to their corresponding handler methods. If a command is not found in the mapping, a default error response is generated.
-
Serialization: The handler utilizes the
SerializerandSerializorFactoryto serialize command responses before sending them back to clients. This ensures proper formatting according to the Redis protocol. -
Error Handling: In case of unsupported commands or errors during command execution, the
CommandHandlergenerates appropriate error responses, maintaining compatibility with the Redis protocol. -
Custom Commands: Developers can extend the
CommandHandlerto add support for custom commands or modify the behavior of existing ones. The handler is designed to be extensible and adaptable to different use cases.
The CommandHandler class follows the Command Handler pattern, where each type of Redis command is encapsulated within a command class. It decouples the sender of the command (received from clients) from the object that processes the command, allowing for extensibility and easy addition of new commands.
The SerializorFactory class implements the Factory Pattern. It provides an interface for creating different types of serializers based on the type of data to be serialized. This promotes flexibility in choosing the appropriate serialization strategy at runtime.
# Example usage of the SerializorFactory
serializer_factory = SerializorFactory()
string_serializer = serializer_factory.create_serializor(str)The Serializer class and its strategies (e.g., StringSerializer, NumberSerializer, ArraySerializer) embody the Strategy Pattern. This pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. The Serializer class can switch between different serialization strategies dynamically.
handler = CommandHandler(redis_cache_instance) handler.serializer.set_strategy(serializer_factory.create_serializor(str))
The CommandHandler is instantiated in the main.py file and is responsible for processing commands received from clients. The handle_command method is called for each incoming command, and the appropriate response is returned.
# Creating an instance of the CommandHandler
handler = CommandHandler(redis_cache_instance)
# Handling a command (e.g., SET key value)
response = handler.handle_command(["SET", "key", "value"])
print(response)The cache.py file houses the RedisCache class, which serves as the in-memory cache for the Redis-like cache server. It manages key-value pairs, supports various data types, and handles expiration based on time-to-live (TTL). Additionally, it includes a CacheHolder class responsible for managing multiple client-specific caches.
- File:
cache.py - Responsibility: Implementation of the in-memory cache and client-specific cache management.
- Dependencies: Uses asyncio for asynchronous task scheduling.
The RedisCache class represents the in-memory cache and is instantiated for each connected client. It includes methods for key-value operations, TTL-based expiration, and task scheduling for cleanup.
is_key_existing(keys): Checks the existence of keys in the cache.get_by_key(key): Retrieves a value by key.delete_by_key(key): Deletes a key-value pair.delete_by_keys(keys): Deletes multiple key-value pairs.set_key_value(key, value): Sets a key-value pair with an optional TTL.lrange(values): Retrieves a range of elements from a list.append_to_tail(values): Appends elements to the tail of a list.append_to_head(values): Appends elements to the head of a list.decrement(key): Decrements the value associated with a key.increment(key): Increments the value associated with a key.schedule_cleanup(key, ttl): Asynchronously schedules the cleanup of a key after a specified TTL.save(): Persists the cache data to the file system.
The CacheHolder class follows the Singleton pattern and manages instances of the RedisCache class for different clients. It ensures that each client has a unique cache and can acquire it based on the client's name. The class also provides methods to check cache existence, both in-memory and on the file system.
get_client_cache(name): Retrieves a client-specific cache from memory.get_client_fs(name): Retrieves a client-specific cache from the file system.is_existing(name): Checks if a client-specific cache exists in memory.is_in_fs_existing(name): Checks if a client-specific cache exists on the file system.acquire_cache(name): Acquires a client-specific cache, creating a new one if necessary.
-
Singleton Pattern: The
CacheHolderclass follows the Singleton pattern, ensuring a single instance manages all client-specific caches.- Implementation: The Singleton pattern is implemented using a private class variable
_instanceand a__new__method that creates a new instance only if_instanceisNone. This ensures that there is only one instance ofCacheHolderthroughout the application.
- Implementation: The Singleton pattern is implemented using a private class variable
-
Async Task Scheduling: Asynchronous task scheduling is used for TTL-based key cleanup. This avoids blocking the event loop during waiting periods.
-
File System Persistence: Cache data is persisted to the file system using JSON, allowing data to be loaded from the file system on server restarts.
The RedisCache and CacheHolder classes are essential components of the Redis-like cache server. They handle client-specific caching, TTL-based expiration, and data persistence.
The parser.py file contains the CommandParser class, responsible for parsing commands received by the Redis-like cache server. It implements handlers for different types of commands, including simple strings, errors, integers, strings, and arrays.
- File:
parser.py - Responsibility: Parsing commands sent to the cache server.
- Dependencies: None.
The CommandParser class is designed to interpret and handle various types of Redis-like commands. It uses different methods to handle different command prefixes, such as + for simple strings, - for errors, : for integers, $ for strings, and * for arrays.
parse_command(command): Parses the given command and dispatches it to the appropriate handler.handle_simple_string(input): Handles simple string commands.handle_error(input): Handles error commands.handle_integer(input): Handles integer commands.handle_string(input): Handles string commands.handle_array(input): Handles array commands.
-
Command Dispatching: The
CommandParseruses a dictionary (command_mappings) to dispatch commands to specific handler methods based on their prefixes. -
Error Handling: If an unknown command prefix is encountered, the
command_not_foundmethod is invoked, providing an error message.
The CommandParser is a crucial component of the Redis-like cache server, responsible for interpreting and directing incoming commands to the appropriate handlers.
This script is part of the Redis-like cache server project and is licensed under the MIT License. See the project's LICENSE file for details.
Feel free to customize the server and add more features based on your requirements. The project provides a basic framework that you can extend and enhance.