## Motivation
Since Redis 6.0, a new serialization protocol format (v3) is available that gives richer semantic reasoning behind the different types to enable commands to better understand the return types to provide in their programming language.
In addition, the `RESPTranslator` type is going to see more direct usage, and the current API doesn't make read well.
## Changes
- Add: Internal `RESPVersion` enum that the `RESPTranslator` will start to use
- Rename: `RESPTranslator.parseBytes` to `RESPTranslator.read(from:)`
## Motivation
The API for establishing the configuration of a connection pool had a lot of jargon and properties that developers had issues keeping straight and understanding what each does.
This commit provides first-class API support for concepts such as retry strategies, and how the pool handles connection counts.
## Changes
- Add: New ConnectionCountBehavior for determining leaky / non-leaky behavior
- Add: New ConnectionRetryStrategy for allowing customization of retry behavior
- Change: RedisConnection.defaultPort to be a computed property
- Change: The logging keys of pool connection retry metadata
- Rename: Several configuration properties to drop prefixes or to be combined into new structures
## Result
Developers should have a much better experience exploring the available configuration options for pools and connections, being able to understand how each piece works with the underlying system.
There are many times that developers want exact control over which EventLoop will be executing their chained EventLoopFuture callbacks
and which Logger will do the logging in calls deep within RediStack.
All commands will now accept an optional EventLoop and Logger to hop to, and using the logger for desired logs.
Motivation:
`RedisKeyLifetime` already has "RedisKey" as a prefix so it naturally fits as a nested type.
Modifications:
- Change: `RedisKeyLifetime` to be nested in `RedisKey` and named `Lifetime`
- Rename: `RedisKeyLifetime.Lifetime` to `Duration`
- Deprecate: `RedisKeyLifetime` and the nested type `Lifetime`
Result:
The global namespace is a little less cluttered with the types falling naturally where they already are.
Motivation:
The original implementation of Logging was done in more haste than should have been, without proper attention given to the semantic requirements.
As the Swift ecosystem has matured a bit, lessons have been learned on handling metadata and passing of external context into internal subcomponents.
A mixture of the "protocol-based context passing" and "explicit context passing" patterns have been adopted.
Both patterns are more fully described in the Swift forum discussion: https://forums.swift.org/t/the-context-passing-problem/39162
Modifications:
- Add: `RedisLogging` namespace with references to static keys and labels that are used for Logging throughout the library
- Add: `Logger` static computed properties to access the Logger prototypes used in connection and connection pools
- Add: `RedisClientWithUserContext` protocol and `UserContextRedisClient` types to assist with wrapping client types for custom logger contexts
- Remove: `logger` property from `RedisClient` requirements
- Change: Many log statements to have higher or lower log levels for their appropriate context
- Change: `RedisConnection` and `RedisConnectionPool` to conform to `RedisClientWithUserContext`
- Change: `logging(to:)` protocol requirement to return a `RedisClient` existential
- Change: ConnectionPool to explicitly pass a logger instance around for pooling methods
Result:
Logging in RediStack will now have a stronger contract of where and how logs will be generated and which context will be used.
Fixes#79 and #74
Motivation:
Users of Redis will frequently want to be able to run queries in
parallel, while bounding the number of connections they use. They will
also often want to be able to reuse connections, without having to
arrange to manage those connections themselves. These are jobs usually
done by a Connection Pool.
This new connection pool will conform to `RedisClient` so a pool of clients and a single connection are interchangeable.
Connection Pools come in a wide range of shapes and sizes. In NIO
applications and frameworks, there are a number of questions that have
to be answered by any pool implementation:
1. Is the pool safe to share across EventLoops: that is, is its
interface thread-safe?
2. Is the pool _tied_ to an EventLoop: that is, can the pool return
connections that belong on lots of event loops, or just one?
3. If the pool is not tied to an EventLoop, is it possible to influence
its choice about what event loop it uses for a given connection?
Question 1 is straightforward: it is almost always a trivial win to
ensure that the public interface to a connection pool is thread-safe.
NIO makes it possible to do this fairly cheaply in the case when the
pool is only used on a single loop.
Question 2 is a lot harder. Pools that are not tied to a specific
EventLoop have two advantages. The first is that it is easier to bound
maximum concurrency by simply configuring the pool, instead of needing
to do math on the number of pools and the number of event loops. The
second is that non-tied pools can arrange to keep busy applications
close to this maximum concurrency regardless of how the application
spreads its load across loops.
However, pools that are tied to a specific EventLoop have advantages
too. The first is one of implementation simplicity. As they always serve
connections on a single EventLoop, they can arrange to have all of their
state on that event loop too. This avoids the need to acquire locks on
that loop, making internal state management easier and more obviously
correct without having to worry about how long locks are held for.
The second advantage is that they can be used for latency sensitive
use-cases without needing to go to the work of (3). In cases where
latency is very important, it can be valuable to ensure that any Channel
that needs a connection can get one on the same event loop as itself.
This avoids the need to thread-hop in order to communicate between the
pooled connection and the user connection, reducing the latency of
operations.
Given the simplicity and latency benefits (which we deem particularly
important for Redis use-cases), we concluded that a good initial
implementation will be a pool that has a thread-safe interface, but is
tied to a single EventLoop. This allows a compact, easy-to-verify
implementation of the pool with great low-latency performance and simple
implementation logic, that can still be accessed from any EventLoop in
cases when latency is not a concern.
Modifications:
- Add new internal `ConnectionPool` object
- Add new `RedisConnectionPool` object
- Add new `RedisConnectionPoolError` type
- Add tests for new types
Results:
Users will have access to a pooled Redis client.
Motivation:
The TTL and PTTL commands are missing.
Modifications:
- Add TTL and PTTL commands
- Add integration tests
Result:
- Users can query the ttl in seconds or milliseconds of a key
Motivation:
parseInteger did not distinguish between not having enough bytes for an
integer and not being able to parse the integer that was present. This
was a bit tricky for code internally, where some call sites had extra
code looking for spooky action at a distance in order to determine if
the integer failed to parse.
This is unnecessary: parseInteger is sufficiently aware of what's going
on to address this problem itself.
Modifications:
- Added a new parser error (acceptable as we haven't tagged 1.0 yet).
- Throw it from parseInteger if the integer is invalid.
Result:
parseInteger clearly communicates if the integer failed to parse.
Motivation:
ByteBufferView is not zero indexed, but parseSimpleString assumes it is.
Modifications:
- Correctly compute on the distance between two indices.
- New, somewhat contrived, test case.
Result:
No functional change: because RediStack assumes the remote peer will
always correctly terminate with /r/n, there is no point at which this
code could misbehave in the current implementation. However, with small
changes it is possible to trigger it, as the new test demonstrates.
Motivation:
While working to add more test coverage with `RESPTranslator`, it was made apparent that a `.bulkString(.none)` is impossible to create directly with the `RESPValue` initializers, even though it is a reasonable possibility.
Additionally, forcing all integer types to have to be stored in an `Int` is unnecessarily restrictive.
Modifications:
- Change `RESPValue.init(bulk:)` initializers to accept `Optional` instances
- Change `RESPValue.init(bulk:)` for `Int` initializer to be generic on `FixedWidthInteger`
Result:
Converting types to and from `RESPValue` should be more bi-directional and seamless.
Motivation:
Diagnostics for why `.bulkString` parses might fail were weak, and edge cases fell through gaps in coverage were found.
Modifications:
Added new cases to `RESPTranslator.ParsingError` for `.bulkString` parsing with additional test coverage.
Result:
Users should have better diagnostics for bogus data or failed parsing state.
Motivation:
While working on issue #56, it was forgotten to add the new test cases to the linux manifest file.
Modifications:
Update the linux manifests to include all current unit tests
Result:
All written unit tests should be ran on Linux
Motivation:
For users looking to contribute, and for those looking to validate the library, it was unclear what tests require an actual connection to a Redis instance in order to run.
Modifications:
Add a `RediStackIntegrationTests` that takes all tests that require a Redis instance in order to run.
Result:
Those looking to run just unit tests, or contribute new tests, can now directly point to a specific testTarget as defined in the Package manifest.
Motivation:
There is a reasonable way to compare if two `RedisErrors` are equal, which was seen as needed in the `Equatable` conformance for `RESPValue`.
Modifications:
Added `Equatable` conformance for `RedisError` by comparing the messages.
Result:
Two `RedisError` instances are now equatable.
Motivation:
While working on unit tests the need for conformance to `Equatable` for `RESPValue` has been needed a few times and it was decided to make it public.
Modifications:
Added conformance to `Equatable` for `RESPValue` with unit test.
Result:
Users should now be able to compare two `RESPValue` instances for equality.
Motivation:
A handful of times, unit tests were forgotten to be added to the `allTests` extension, or were incorrectly copy/pasted.
Modifications:
Remove manual entries of `allTest` and use generated result from `swift test --generate-linuxmain`
Result:
There should be proper test parity between macOS and Linux.
Motivation:
Issue #60 called for improving the type safety of the options available for the `zadd` command, and MR !70 made some great headway, but attempted to cram too much into a single enum.
Modifications:
- Break the `RedisSortedSetAddOption.returnChangedCount` value into an additional boolean param
Result:
Using `zadd` should now be more straight forward, while being type safe.