-
Notifications
You must be signed in to change notification settings - Fork 25
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
[#167064659] Optimistic lock on update user metadata #577
base: master
Are you sure you want to change the base?
Conversation
Affected stories
New dependencies added: async-semaAuthor: Olli Vanhoja Description: Semaphore using `async` and `await` Homepage: https://github.com/zeit/async-sema
|
*/ | ||
public async set( | ||
user: User, | ||
payload: UserMetadata | ||
): Promise<Either<Error, boolean>> { | ||
// In order to work properly, optimistic lock needs to be initialized on different |
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.
where did you find this info ?
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 tested locally with the following test and the output was unexpected:
it.only("Test Optimistic Lock", async () => {
const REDIS_CLIENT = createSimpleRedisClient("redis://localhost");
const ums = new RedisUserMetadataStorage(REDIS_CLIENT);
const meta = {
metadata,
version: 8
};
const metaR = await Promise.all([
ums.set(aValidUser, meta),
ums.set(aValidUser, meta),
ums.set(aValidUser, meta)
]);
console.log(metaR); // [ right(true), right(true), right(true) ]
});
So, I've read the example (second code snippet) here with the keyword another client into the description and with the modification (const duplicatedRedisClient = this.redisClient.duplicate();
) the result was as expected [ left(Error: Concurrent write operation),right(true),left(Error: Concurrent write operation) ]
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.
kudos, that's a good test
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 test is good, but as far as I understand (and the test seems to confirm it) the problem of concurrent updates happens only when you have mutiple redis clients trying to update the same resource (ie. different instances of the backend nodejs process / pod). so while it's right to use multiple clients inside the test, it is not right to duplicate the client inside the backend code (you just have to call watch before multi). this states even if it is the same client that tries to update the same resource (watch + multi "locks" the resource anyway, you don't have to duplicate the client).
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.
@gunzip I think that watch + multi scenario doesn't work on the same redis client instance, so if we don't duplicate the client when we start an Optimistic Lock on a key and a race condition happens on the same process/pod (it can occur) then OL doesn't work.
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.
Let's try to recap: set()
fails in case the value (in redis) that matches the key ${userMetadataPrefix}${user.fiscal_code}
is changed after the call to watch()
and before the call to multi()
[1]. This means that another call to set()
starts while the first call is till executing. This is the test code:
it.only("Test Optimistic Lock", async () => {
const REDIS_CLIENT = createSimpleRedisClient("redis://localhost");
const ums = new RedisUserMetadataStorage(REDIS_CLIENT);
const meta = {
metadata,
version: 8
};
const metaR = await Promise.all([
ums.set(aValidUser, meta),
ums.set(aValidUser, meta),
ums.set(aValidUser, meta)
]);
console.log(metaR); // [ right(true), right(true), right(true) ]
});
Sorry if I'm nitpicking here, but I wonder, how can you tell from this code if the three calls to set()
are interleaved in such a way that [1] is satisfied ? I mean, the test may returns
[ right(true), right(true), right(true) ]
not because it cannot catch the race condition but because the race condition does not happen at all (or I'm missing 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.
Ok, I understand the point. To be sure of that I'll add a timeout before the multi
phase, so will be certain that all tree calls happen simultaneously.
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.
Here the updated code:
public async set(
user: User,
payload: UserMetadata
): Promise<Either<Error, boolean>> {
// In order to work properly, optimistic lock needs to be initialized on different
// redis client instances
// const duplicatedRedisClient = this.redisClient.duplicate();
return await new Promise<Either<Error, boolean>>(resolve => {
this.redisClient.watch(
`${userMetadataPrefix}${user.fiscal_code}`,
err => {
if (err) {
return resolve(left(err));
}
this.redisClient.get(
`${userMetadataPrefix}${user.fiscal_code}`,
(err1, response) => {
if (err1 || response === null) {
resolve(left(err1 || metadataNotFoundError));
} else {
setTimeout(() => {
this.redisClient
.multi()
.set(
`${userMetadataPrefix}${user.fiscal_code}`,
JSON.stringify(payload)
)
.exec((err2, results) => {
if (err2) {
return resolve(left(err2));
}
if (results === null) {
return resolve(left(concurrentWriteRejectionError));
}
resolve(this.singleStringReply(err, results[0]));
});
}, 1000);
}
}
);
}
);
});
}
As before, the result was [ right(true), right(true), right(true) ]
😄
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.
-DELETED-
Ok lets proceed and merge this PR (I'm still not convinced that there could be issues aith a single client but I'm doing some test myself : )
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 a found a comment that confirms what @BurnedMarshal says: redis/node-redis#1320 (comment)
We need one client for every set()
call indeed which is sub-optimal since it adds some overhead (as stated in the reply).
*/ | ||
public async set( | ||
user: User, | ||
payload: UserMetadata | ||
): Promise<Either<Error, boolean>> { | ||
// In order to work properly, optimistic lock needs to be initialized on different | ||
// redis client instances |
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.
lgtm, please add a link to redis/node-redis#1320 (comment)
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.
Fixed in 809bc57
Codecov Report
@@ Coverage Diff @@
## master #577 +/- ##
==========================================
+ Coverage 82.99% 83.29% +0.29%
==========================================
Files 46 46
Lines 1347 1371 +24
Branches 236 236
==========================================
+ Hits 1118 1142 +24
Misses 219 219
Partials 10 10
Continue to review full report at Codecov.
|
lgtm so far but is has conflcts with master. a question: why we need a mutex to delete the fiscal code from the set here ? private async resetOperation(fiscalCode: FiscalCode): Promise<void> {
await this.mutex.acquire();
this.activeClients.delete(fiscalCode);
this.mutex.release();
} |
37fcb12
to
6df6a4f
Compare
@gunzip you are right. This mutex is useless for the nature of the shared variable (on/off values). |
@cloudify this lgtm. @BurnedMarshal added a mutex since duplicating the redis client for every update is an expensive ops (it opens a new socket every time). now the client waits to unlock a mutex before checking if there's another client doing the same operation. if it's ok for you we can proceed and merge this one. |
return left(getUserMetadataResult.value); | ||
// In order to work properly, optimistic lock needs to be initialized on different | ||
// redis client instances @see https://github.com/NodeRedis/node_redis/issues/1320#issuecomment-373200541 | ||
await this.mutex.acquire(); |
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 suggest to extract this logic into a kind of client pool 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.
a couple of suggestions on async-sema
:
- review the defaults, are they good for us?
- use
tryAcquire
in place ofacquire
to avoid accumulating "concurrent threads", if the mutex can't be acquired, fail with an HTTP 429
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 suggest to extract this logic into a kind of client pool class
can you please elaborate a little bit more ? do you mean to create (at startup) a pool of N redisclients to reuse and change the mutex(1) to mutex(N) ?
do you think we can merge this PR as-is and implement the pooling in another PR ? (after the beta release date)
) | ||
.chain(fromEither) | ||
.run(); | ||
hasActiveClient |
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.
this should call a kind of dispose
method in the aforementioned client pool
No description provided.