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.