Redis
Description
<EsRedisApp> is a comprehensive, high-performance Redis client for the VAST Platform Smalltalk environment. It provides a robust, developer-friendly API for interactingwith a Redis server, abstracting away the complexities of the underlying C library and Redis protocol.
The application is designed with a clean, layered architecture, separating the low-level platform interface from the high-level client logic. It offers first-class support for essential Redis features, including synchronous and asynchronous command execution, pipelining, and a flexible type-handling system for replies. For concurrent applications, the <EsRedisClientPool> provides robust and efficient connection management.
Architecture
The Redis client is composed of two primary applications:
EsHiredisPI (Sub-application)
This is the Platform Interface (PI) layer, responsible for the Foreign Function Interface (FFI) to the native hiredis C library. It handles all low-level details, such as C structure memory mapping, function calls, and constant definitions. This layer completely isolates the rest of the application from platform-specific code.
EsRedisApp (Main Application)
This application contains the core client logic and the public-facing API. It builds upon the <EsHiredisPI> to provide a high-level, object-oriented interface for developers.
Key Features
Concurrency with EsRedisClientPool
For multi-process or multi-threaded applications, the <EsRedisClientPool> is the primary tool for achieving high throughput and managing connections safely. It is a resource manager, not a direct replacement for <EsRedisClient>. Its core API is the #executeWithClient: method, which leases a dedicated client connection for a block of code and guarantees it is returned to the pool, even in the case of errors.
Example:
| pool result |
pool := EsRedisClientPool host: '127.0.0.1' port: 6379 size: 10.
pool connect.
result := pool executeWithClient: [:client |
client strings set: 'mykey' value: 'myvalue'.
client strings get: 'mykey'
].
self assert: (result = 'myvalue').
pool disconnect.
##### Synchronous and Asynchronous API
The client can be used in a blocking (synchronous) or non-blocking (asynchronous)
manner. All command APIs seamlessly support both modes.
Synchronous Example:
"The command blocks until the reply is received."
client strings set: 'sync-key' value: 'sync-value'.
self assert: (client strings get: 'sync-key') equals: 'sync-value'.
Asynchronous Example:
| future |
client isAsync: true.
"The command returns an EsFuture immediately."
future := client strings set: 'async-key' value: 'async-value'.
future := future then: [ client strings get: 'async-key' ].
self assert: future waitFor equals: 'async-value'.
##### Pipelining
Commands can be batched and sent to the server at once using the #pipeline: method, significantly
reducing network latency for bulk operations. The block receives an `<EsRedisPipeline>` instance to queue commands on.
When operating in asynchronous mode, each command sent inside the pipeline block returns its own `<EsFuture>`,
allowing you to reference and coordinate individual operations.
Example:
| results |
results := client pipeline: [:pipe |
pipe strings set: 'pipe_key1' value: 'pipe_val1'.
pipe lists rpush: 'pipe_list' values: #('a' 'b' 'c').
pipe strings get: 'pipe_key1'.
pipe lists llen: 'pipe_list'.
].
self assert: results equals: #('OK' 3 'pipe_val1' 3).
##### Transactions
Commands can be executed atomically as a single transaction using the #transaction: method. This guarantees
that no other client can interfere during the execution of the command block. The block receives an `<EsRedisTransaction>`
instance to queue commands on. A transaction can be explicitly aborted by calling #discard on the transaction object.
Example:
| results |
client strings set: 'balance' value: 100.
results := client transaction: [:tx |
tx strings incrby: 'balance' amount: 50.
tx strings decrby: 'balance' amount: 20.
].
"results => #(150 130)"
self assert: (client strings get: 'balance') equals: '130'.
##### Command Groups
The Redis command set is organized into logical groups based on data types (e.g., Strings, Hashes, Lists, Sets).
This makes the API intuitive and easy to navigate.
Example:
"Access different command groups from the same client instance."
client strings set: 'user:1:name' value: 'Alice'.
client hashes hset: 'user:1:profile' field: 'email' value: 'alice@example.com'.
client sets sadd: 'online_users' members: 'user:1'.
##### Centralized Argument Conversion
All Smalltalk objects passed as command arguments are automatically and consistently
converted to the correct binary-safe wire format just before being sent to the server.
Example:
"The integer 100 is automatically converted to the string '100'."
client strings set: 'counter' value: 100.
"A block's return value is used, deferring the serialization cost."
client strings set: 'my-object' value: [ ObjectDumper new unload: anObject ].
##### Flexible Reply Handling
The client provides multiple levels of control over how replies are converted into Smalltalk objects.
##### Default Reply Containers
The client can be configured to return Redis string replies as different Smalltalk
objects (`<String>`, `<ByteArray>`, `<UnicodeString>`) to easily handle various types of data.
Example:
"Get binary data as a ByteArray"
client defaultStringReplyContainer: ByteArray.
client strings set: 'binaryKey' value: #[1 2 3 255].
binaryData := client strings get: 'binaryKey'.
self assert: (binaryData isKindOf: ByteArray).
"Get UTF-8 data as a UnicodeString"
client defaultStringReplyContainer: UnicodeString.
client strings set: 'unicodeKey' value: '??'.
unicodeData := client strings get: 'unicodeKey'.
self assert: (unicodeData isKindOf: UnicodeString).
#### Scoped Reply Overrides
For fine-grained control, the usingReplyConverter:do: method allows you to temporarily change the reply converter for any command within a
specific block of code. This is useful for handling special cases or working with custom domain objects without changing the
client's global settings.
- *usingReplyConverter:do:* (User-Friendly): Use a `<Class>` or a `<Block>` as a converter. When a block is used, it receives the already-converted
default Smalltalk object for further transformation.
| upperCase | "The block receives the default reply (a String) and transforms it." upperCase := client usingReplyConverter: [:replyString :context | replyString asUppercase] do: [:c | c server echo: 'hello' ]. self assert: (upperCase = 'HELLO').
- *usingRawReply:do:* (High-Performance): For performance-critical tasks like deserializing binary data, this method provides your
block with the raw, unconverted reply object. This allows for direct access to the underlying data (e.g., `byteArrayValue`) and avoids
creating intermediate objects.
| mySet | "Use the raw reply handler to deserialize an object efficiently" mySet := client usingRawReply: [:rawReply :context | ObjectLoader new loadFromStream: rawReply byteArrayValue readStream ] do: [:c | c strings get: 'myset' ]. self assert: (mySet = (Set with: 1 with: 2)). `
Class Methods
None
Instance Methods
None