Skip to content

Distributed lock for calendar callbacks#20252

Closed
getvictor wants to merge 11 commits intomainfrom
victor/19352-distributed-lock-2
Closed

Distributed lock for calendar callbacks#20252
getvictor wants to merge 11 commits intomainfrom
victor/19352-distributed-lock-2

Conversation

@getvictor
Copy link
Copy Markdown
Member

@getvictor getvictor commented Jul 8, 2024

#19352
Adding distributed lock for Google calendar callback.

Checklist for submitter

If some of the following don't apply, delete the relevant line.

  • Added/updated tests
  • Manual QA for all new/changed functionality

if fleet.IsNotFound(err) {
// We could try to stop the channel callbacks here, but that may not be secure since we don't know if the request is legitimate
level.Warn(svc.logger).Log("msg", "Received calendar callback, but did not find corresponding event in database", "event_uuid",
level.Info(svc.logger).Log("msg", "Received calendar callback, but did not find corresponding event in database", "event_uuid",
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may occur when a calendar event is created, and another change to the calendar occurs at the same time, triggering a callback. The callback comes in before we saved the event in the DB (or before it replicated to the replica). This is OK since we don't expect the event to be immediately modified upon creation.

@getvictor getvictor marked this pull request as ready for review July 8, 2024 21:19
@getvictor getvictor requested review from a team, gillespi314, lucasmrod and roperzh as code owners July 8, 2024 21:19
@lukeheath lukeheath added the :ai Request AI PR review label Jul 8, 2024
@lukeheath
Copy link
Copy Markdown
Member

@getvictor I'm trying out the PR review bot on this.

Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Summary

  • Introduced distributed lock mechanism for Google calendar callbacks (cmd/fleet/serve.go, ee/server/service/calendar.go, server/cron/calendar_cron.go)
  • Added distributedLock field to Service struct and updated initialization (ee/server/service/service.go)
  • Implemented Redis-based distributed lock (server/service/redis_lock/redis_lock.go, server/service/redis_lock/redis_lock_test.go)
  • Updated tests to include distributed lock functionality (ee/server/service/mdm_external_test.go, server/cron/calendar_cron_test.go)
  • Modified utility functions to support distributed lock (server/service/testing_utils.go)

14 file(s) reviewed, 50 comment(s)
Edit PR Review Bot Settings

Comment thread cmd/fleet/serve.go
@@ -50,6 +50,7 @@ import (
"github.com/fleetdm/fleet/v4/server/pubsub"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ info: Imported the redis_lock package to support distributed locking.

Comment thread cmd/fleet/serve.go
@@ -691,6 +692,7 @@ the way that the Fleet server works.
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ info: Initialized a distributedLock variable to hold the lock instance.

Comment thread cmd/fleet/serve.go
@@ -718,6 +720,7 @@ the way that the Fleet server works.
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ info: Created a new distributed lock using redis_lock.NewLock(redisPool).

Comment thread cmd/fleet/serve.go
@@ -730,6 +733,7 @@ the way that the Fleet server works.
ssoSessionStore,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ info: Passed the distributedLock to the eeservice.NewService function.

Comment thread cmd/fleet/serve.go
@@ -870,7 +874,7 @@ the way that the Fleet server works.
} else {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ info: Updated the calendar schedule to use the distributedLock.

Comment on lines +96 to +106
foundNewEvents := false
for _, item := range list.(*calendar.Events).Items {
created, err := time.Parse(time.RFC3339, item.Created)
if err != nil {
log.Fatalf("Unable to parse event created time: %v", err)
}
if created.After(now) {
// Found events created after we started deleting events, so we should stop
foundNewEvents = true
continue // Skip this event but finish the loop to make sure we don't miss something
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ info: Added logic to check if an event was created after the script started running and skip its deletion.

Comment on lines +122 to +125
pageToken = list.(*calendar.Events).NextPageToken
if pageToken == "" || foundNewEvents {
break
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ info: Handled NextPageToken to continue pagination until all events are processed or new events are found.


numberMoved := 0
var maxResults int64 = 1000
pageToken := ""
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ info: Added pageToken to handle pagination for large sets of events.

Comment on lines +108 to +118
foundNewEvents := false
for _, item := range list.(*calendar.Events).Items {
created, err := time.Parse(time.RFC3339, item.Created)
if err != nil {
log.Fatalf("Unable to parse event created time: %v", err)
}
if created.After(now) {
// Found events created after we started moving events, so we should stop
foundNewEvents = true
continue // Skip this event but finish the loop to make sure we don't miss something
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ info: Added logic to skip events created after the script started running to avoid moving new events.

Comment on lines +137 to +139
pageToken = list.(*calendar.Events).NextPageToken
if pageToken == "" || foundNewEvents {
break
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ info: Ensure pageToken is updated and check for new events to decide when to break the loop.

Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Summary

(updates since last review)

  • Integrated distributed lock mechanism for calendar callbacks in server/cron/calendar_cron.go
  • Ensured lock acquisition and release around calendar event processing
  • Updated cron job functions to handle lock reservation and event updates

1 file(s) reviewed, 3 comment(s)
Edit PR Review Bot Settings

logger kitlog.Logger,
) error {

// Try to acquire the lock. Lock is needed to ensure calendar callback is not processed for this event at the same time.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ info: Acquiring the lock to ensure calendar callback is not processed concurrently.


// Remove event from the queue so that we don't process this event again.
// Note: This item can be added back to the queue while we are processing it.
err = distributedLock.RemoveFromSet(ctx, calendar.QueueKey, eventUUID)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ info: Removing event from the queue to prevent reprocessing.

return errors.New("could not acquire calendar lock in time")
}
}
defer func() {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ info: Ensure locks are released after processing to avoid deadlocks.

@lukeheath
Copy link
Copy Markdown
Member

Bot is chatty. Should I disable the "info" comments?

Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Summary

(updates since last review)

  • Introduced distributed lock mechanism for Google Calendar callbacks in ee/server/service/calendar.go
  • Ensured lock acquisition and release around event processing
  • Added events to a queue if the lock is reserved
  • Enabled asynchronous processing of events when needed

No major changes found since last review.

1 file(s) reviewed, 6 comment(s)
Edit PR Review Bot Settings

Comment on lines +74 to +81
lockValue, reserved, err := svc.getCalendarLock(ctx, eventUUID, true)
if err != nil {
return err
}
// If lock has been reserved by cron, we will need to re-process this event in case the calendar event was changed after the cron job read it.
if lockValue == "" && !reserved {
// We did not get a lock, so there is nothing to do here
return nil
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ info: Ensures that if the lock is reserved, the event will be re-processed to handle any changes made after the cron job read it.

Comment on lines +92 to +97
// Remove event from the queue so that we don't process this event again.
// Note: This item can be added back to the queue while we are processing it.
err = svc.distributedLock.RemoveFromSet(ctx, calendar.QueueKey, eventUUID)
if err != nil {
return ctxerr.Wrap(ctx, err, "remove calendar event from queue")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ info: Removes the event from the queue to prevent re-processing while it is being handled.

Comment on lines +198 to +206
func (svc *Service) releaseCalendarLock(ctx context.Context, eventUUID string, lockValue string) {
ok, err := svc.distributedLock.ReleaseLock(ctx, calendar.LockKeyPrefix+eventUUID, lockValue)
if err != nil {
level.Error(svc.logger).Log("msg", "Failed to release calendar lock", "err", err)
}
if !ok {
// If the lock was not released, it will expire on its own.
level.Warn(svc.logger).Log("msg", "Failed to release calendar lock")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ info: Releases the distributed lock and handles potential errors.

Comment on lines +209 to +219
func (svc *Service) getCalendarLock(ctx context.Context, eventUUID string, addToQueue bool) (lockValue string, reserved bool, err error) {
// Check if lock has been reserved, which means we can't have it.
reservedValue, err := svc.distributedLock.Get(ctx, calendar.ReservedLockKeyPrefix+eventUUID)
if err != nil {
return "", false, ctxerr.Wrap(ctx, err, "get calendar reserved lock")
}
reserved = reservedValue != nil
if reserved && !addToQueue {
// We flag the lock as reserved.
return "", reserved, nil
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ info: Acquires the distributed lock and adds the event to the queue if the lock is not available.

Comment on lines +256 to +261
func (svc *Service) processCalendarAsync(ctx context.Context, eventIDs []string) {
defer func() {
asyncMutex.Lock()
asyncCalendarProcessing = false
asyncMutex.Unlock()
}()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ info: Processes calendar events asynchronously from the queue.

Comment on lines +282 to +292
func (svc *Service) processCalendarEventAsync(ctx context.Context, eventUUID string) bool {
lockValue, _, err := svc.getCalendarLock(ctx, eventUUID, false)
if err != nil {
level.Error(svc.logger).Log("msg", "Failed to get calendar lock", "err", err)
return false
}
if lockValue == "" {
// We did not get a lock, so there is nothing to do here
return true
}
defer svc.releaseCalendarLock(ctx, eventUUID, lockValue)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ info: Processes individual calendar events asynchronously, ensuring locks are managed correctly.

Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Summary

(updates since last review)

  • Introduced distributed lock mechanism in server/cron/calendar_cron.go
  • Acquired and released locks around calendar event processing
  • Handled lock acquisition timeouts
  • Ensured events are not reprocessed
  • Prevented race conditions and ensured data consistency

1 file(s) reviewed, 1 comment(s)
Edit PR Review Bot Settings

Comment on lines +357 to +361
case <-done:
// Lock was acquired.
if err != nil {
return fmt.Errorf("try to acquire calendar lock: %w", err)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧠 logic: Ensure that err is properly checked before returning it to avoid potential nil pointer dereference.

level.Info(svc.logger).Log("msg", "Received calendar callback, but did not find corresponding event in database", "event_uuid",
eventUUID, "channel_id", channelID)
return err
return nil
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return nil so that we don't get retries?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to happen often for legitimate reasons that it doesn't make sense to flag it as error. Google calendar doesn't seem to retry on error. Just don't want the load balancer to see a bunch of 4XX errors and customers start complaining about it.

Comment thread server/fleet/calendar.go Outdated
Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Summary

(updates since last review)

  • Introduced Lock interface in /server/fleet/calendar.go for distributed lock management
  • Renamed parameter name to key in /server/service/redis_lock/redis_lock.go for consistency
  • Ensured synchronized operations for Google Calendar callbacks across distributed systems

2 file(s) reviewed, 4 comment(s)
Edit PR Review Bot Settings

Comment thread server/fleet/calendar.go
Comment on lines +45 to +57
type Lock interface {
// AcquireLock attempts to acquire a lock with the given key. value is the value to set for the key, which is used to release the lock.
AcquireLock(ctx context.Context, key string, value string, expireMs uint64) (result string, err error)
// ReleaseLock attempts to release a lock with the given key and value. If key does not exist or value does not match, the lock is not released.
ReleaseLock(ctx context.Context, key string, value string) (ok bool, err error)
// Get retrieves the value of the given key. If the key does not exist, nil is returned.
Get(ctx context.Context, key string) (*string, error)
// AddToSet adds the value to the set identified by the given key.
AddToSet(ctx context.Context, key string, value string) error
// RemoveFromSet removes the value from the set identified by the given key.
RemoveFromSet(ctx context.Context, key string, value string) error
// GetSet retrieves a slice of string values from the set identified by the given key.
GetSet(ctx context.Context, key string) ([]string, error)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ info: Introduced a new Lock interface for managing distributed locks, which includes methods for acquiring, releasing, and managing locks and sets.

return fleet.Lock(lock)
}

func (r *redisLock) AcquireLock(ctx context.Context, key string, value string, expireMs uint64) (result string, err error) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ info: Renamed parameter name to key for consistency.

return result, nil
}

func (r *redisLock) ReleaseLock(ctx context.Context, key string, value string) (ok bool, err error) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ info: Renamed parameter name to key for consistency.

return members, nil
}

func (r *redisLock) Get(ctx context.Context, key string) (*string, error) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ info: Renamed parameter name to key for consistency.

@lukeheath lukeheath removed the :ai Request AI PR review label Jul 9, 2024
@lukeheath
Copy link
Copy Markdown
Member

Sorry for all the noise in the PR. I'm removing the bot review and will try to disable the "info" comments in the future.

Copy link
Copy Markdown
Member

@lucasmrod lucasmrod left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

Left a couple of questions.

// If lock has been reserved by cron, we will need to re-process this event in case the calendar event was changed after the cron job read it.
if lockValue == "" && !reserved {
// We did not get a lock, so there is nothing to do here
return nil
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this scenario? Should we log here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is when our server could not get a lock (because another server has the lock). In the svc.getCalendarLock method, we added this event to the queue. So, the other server will re-process this event.

I could add a debug log here, but this might happen often in production.

Comment on lines +242 to +251
// Try to acquire the lock again in case it was released while we were adding the event to the queue.
result, err = svc.distributedLock.AcquireLock(ctx, calendar.LockKeyPrefix+eventUUID, lockValue, 0)
if err != nil {
return "", false, ctxerr.Wrap(ctx, err, "acquire calendar lock again")
}

if result == "" {
// We could not acquire the lock, so we are done here.
return "", reserved, nil
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this optimization necessary?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it catches the corner case where an event UUID was added to the queue, but no server actually processes the queue because all the locks have been released.

Comment thread server/fleet/calendar.go
// Lock interface for managing distributed locks.
type Lock interface {
// AcquireLock attempts to acquire a lock with the given key. value is the value to set for the key, which is used to release the lock.
AcquireLock(ctx context.Context, key string, value string, expireMs uint64) (result string, err error)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's document what is expireMs and what does expireMs = 0 mean.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, will add on the UUID branch

Comment on lines +107 to +120
// Now, we need to check if there are any events in the queue that need to be re-processed.
asyncMutex.Lock()
defer asyncMutex.Unlock()
if !asyncCalendarProcessing {
eventIDs, err := svc.distributedLock.GetSet(ctx, calendar.QueueKey)
if err != nil {
return ctxerr.Wrap(ctx, err, "get calendar event queue")
}
if len(eventIDs) > 0 {
asyncCalendarProcessing = true
go svc.processCalendarAsync(ctx, eventIDs)
}
return nil
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any issues with two Fleet instances running this async simultaneously and/or a Fleet instance running the cron simultaneously?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should work with multiple instances running it. They loop through the UUIDs and try to get a lock. If they don't get a lock, they try a different UUID. The load test should cover this.

if err != nil {
if fleet.IsNotFound(err) {
// We found this event when the callback initially came in. So the event may have been removed from DB since then.
return true
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this an issue? (Maybe processCalendarEventAsync should return an error?)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This happens frequently because we create a new UUID when re-creating the event. The old UUID stuck in the queue will hit this. I'll update the comment to:

So the event may have been removed or re-created since then.

Comment thread server/fleet/calendar.go
// AcquireLock attempts to acquire a lock with the given key. value is the value to set for the key, which is used to release the lock.
AcquireLock(ctx context.Context, key string, value string, expireMs uint64) (result string, err error)
// ReleaseLock attempts to release a lock with the given key and value. If key does not exist or value does not match, the lock is not released.
ReleaseLock(ctx context.Context, key string, value string) (ok bool, err error)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@document that returns true if it was released, false otherwise (e.g. when value doesn't match).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will add


// Reference: https://redis.io/docs/latest/commands/sadd/
_, err := conn.Do("SADD", r.testPrefix+key, value)
if err != nil {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double checking: do you need to check errors.Is(err, redigo.ErrNil) here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, redigo.ErrNil is only created when you use certain helper methods like redigo.String


// Reference: https://redis.io/docs/latest/commands/srem/
_, err := conn.Do("SREM", r.testPrefix+key, value)
if err != nil {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double checking: do you need to check errors.Is(err, redigo.ErrNil) here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, see above.


// Reference: https://redis.io/docs/latest/commands/smembers/
members, err := redigo.Strings(conn.Do("SMEMBERS", r.testPrefix+key))
if err != nil {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double checking: do you need to check errors.Is(err, redigo.ErrNil) here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is not documented, but I see the implementation can generate an ErrNil. I will add.

Comment thread server/fleet/calendar.go
// Lock interface for managing distributed locks.
type Lock interface {
// AcquireLock attempts to acquire a lock with the given key. value is the value to set for the key, which is used to release the lock.
AcquireLock(ctx context.Context, key string, value string, expireMs uint64) (result string, err error)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the result a string?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it looks like it should be bool. I'll work on it.

getvictor added a commit that referenced this pull request Jul 10, 2024
…20277)

#19352

Fix for code review comment:
#20156 (comment)

Also includes changes from #20252

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [x] Added/updated tests
- [x] If database migrations are included, checked table schema to
confirm autoupdate
- For database migrations:
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
- [x] Manual QA for all new/changed functionality
getvictor added a commit that referenced this pull request Jul 10, 2024
…20277)

#19352

Fix for code review comment:
#20156 (comment)

Also includes changes from #20252

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [x] Added/updated tests
- [x] If database migrations are included, checked table schema to
confirm autoupdate
- For database migrations:
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
- [x] Manual QA for all new/changed functionality

(cherry picked from commit 7bcd61a)
@getvictor
Copy link
Copy Markdown
Member Author

This PR has been superseded by #20277

@getvictor getvictor closed this Jul 10, 2024
@getvictor getvictor deleted the victor/19352-distributed-lock-2 branch May 8, 2025 22:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants