-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Allow Redis client to be injected #5034
feat: Allow Redis client to be injected #5034
Conversation
@adamnoakes: Thank you for submitting a pull request! Before we can merge it, you'll need to sign the Apollo Contributor License Agreement here: https://contribute.apollographql.com/ |
Thanks, this is on my list for my next PR review day. |
Np, no rush 👍 |
# Conflicts: # packages/apollo-server-cache-redis/src/__mocks__/ioredis.ts
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally looking good but the PR needs to be updated to avoid mget with Redis.Cluster.
} | ||
|
||
async delete(key: string): Promise<boolean> { | ||
return await this.client.del(key) > 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. I've expanded your description of this in the PR description.
|
||
export class RedisClusterCache implements KeyValueCache { | ||
export class RedisClusterCache extends BaseRedisCache { | ||
readonly client: any; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huh, this is basically redefining the superclass field's type to be any
. But what the del/delete issues shows is that we should avoid this. I've pushed a commit which should fix this. Note that I'm using the ioredis type directly instead of defining an interface like you did in BaseRedisCache, which seems fine because it's not part of our package's API.
|
||
this.loader = new DataLoader( | ||
(keys = []) => | ||
Promise.all(keys.map(key => client.get(key).catch(() => null))), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So RedisClusterCache didn't use mget and this appears to have been a conscious choice when RedisClusterCache was added. Various issues such as redis/ioredis#811 suggest this is still a concern in newer versions of ioredis.
So we need to support the non-mget path (perhaps subclasses can provide a protected useMget() method?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've committed a potential solution, that fixes it in the injected client rather than adding logic to work around it in BaseRedisClient class, let me know what you think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On second thought after merging this means you can't actually create your own Redis.Cluster object, so I'd rather move the logic back into the base class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To me it would make sense if you wanted to create your own cluster cache to use the base class, the logic in the current RedisClusterCache seems mainly to work around ioredis, if you were using node-redis with the redis-clustr wrapper for example that already has logic to split multi-key commands. Having the base class allows apollo to just focus on core redis logic and let the consumer of the library handle anything implementation specific.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@adamnoakes Makes sense. I was trying to write the docs for #5088 to show examples and I thought it would be nice to be "all in" on the new class rather than showing the older pass-through implementations, but you can't really do that with Redis.Cluster
. Thus the API change in #5088.
But I see what you mean about other implementations that do support mget and cluster.
What if I kept the structure from #5088 but changed the names to be client
and getOnlyClient
or noMgetClient
or something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm yeah i see, could do it via a flag? like useMGet
that defaults to true. Would be cleaner imo than getOnlyClient/ noMgetClient as the client will likely still have mget (if they are just passing cluster ioredis client in as is for example).
On the topic of going all in on the new class. Would be nice to rename it to just RedisCache when you do the next major/ breaking change release and remove the existing RedisCache/ RedisClusterCache classes as i think they are redundant with this approach.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The flag seems like a nice idea, but I do like the type checking we get by providing two different arguments. I am going to go with noMgetClient though to make it clear that it's about what the client supports, not whether you're in a cluster. See 21f148e
Tracking the major version bump suggestion at #5099
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good, thank you for your help on getting this merged.
On the flag type checking, I was curious so had a play and looks like you could have a flag and type checking using overloads.
interface GetClient {
get: () => void;
}
interface MGetClient {
mget: () => void;
}
function test(client: MGetClient, options?: { useMGet: true }): void;
function test(client: GetClient, options: { useMGet: false }): void;
function test(client: GetClient | MGetClient, options: { useMGet: boolean } = { useMGet: true }): void {
return undefined;
}
test({ mget: () => {}}); // ok
test({ get: () => {}}, { useMGet: false }); // ok
test({ mget: () => {}}, { useMGet: true }); // ok
test({ get: () => {}}); // type error
test({ get: () => {}}, { useMGet: true }); // type error
test({ mget: () => {}}, { useMGet: false }); // type error
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nod yeah, that definitely works, but I think I'm going to go with this way (which provides type checking even if you're being more dynamic, whereas yours only provides full type checking if you're using literal true
or false
).
del: clusterClient.del.bind(clusterClient), | ||
flushdb: clusterClient.flushdb.bind(clusterClient), | ||
mget(...keys: Array<string>): Promise<Array<string | null>> { | ||
return Promise.all(keys.map(key => clusterClient.get(key).catch(() => null))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm raising a pretty big eyebrow at that "ignore all errors" catch, but I see that it's copied from the previous version, so not gonna worry about it.
Thanks! I'll add the changelog entry on |
Add docs and changelog. As part of doing this, realize that the implementation in #5034 makes it annoying to inject a cluster client, so switch the new API to decide about get vs mget based on which kind of client is passed to the base class.
Add docs and changelog. As part of doing this, realize that the implementation in #5034 makes it annoying to inject a cluster client, so switch the new API to decide about get vs mget based on which kind of client is passed to the base class.
Add docs and changelog. As part of doing this, realize that the implementation in #5034 makes it annoying to inject a cluster client, so switch the new API to decide about get vs mget based on which kind of client is passed to the base class.
I've released a prerelease with this fix ( |
This has been released in |
BaseRedisCache
class which contains the existing implementation logic without the client initialisation.RedisCache
andRedisClusterCache
KeyValueCache.delete
should, but the actual implementation returned a number. This was not noticed because of the use of anany
type.)In my opinion,
BaseRedisCache
should really beRedisCache
and the existingRedisCache
+RedisCacheCluster
should either be renamed to identify that they are using the ioredis client, or removed and left for the consumer of the library to implement/ inject the client using this method. But this would obviously be a breaking change so maybe one to think about for the future?I've created an interface for the
RedisClient
rather than rely on the ioredis type as i do not believe apollo should be tied to the ioredis interface.This PR solves the same issue as #4871 in a slightly different way.
It will solve these issues:
I welcome any feedback/ requested changes.