-
Notifications
You must be signed in to change notification settings - Fork 6
Add offline purge of orphaned received shares to the jsoncs3 share manager #656
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| Enhancement: Add offline purge of orphaned received shares to the jsoncs3 share manager | ||
|
|
||
| The jsoncs3 share manager can accumulate orphaned received-share entries when a | ||
| space is deleted but the per-user received.json blocks are not cleaned up (for | ||
| example after a missed SpaceDeleted event). These orphans are removed inline on | ||
| ListReceivedShares, but that happens on the request hot path and can race a | ||
| client deadline. | ||
|
|
||
| This adds `Manager.PurgeOrphanedReceivedShares`, which enumerates users, detects | ||
| received-share entries whose share no longer exists in the provider cache and | ||
| removes them in a single batched persist per user and space | ||
| (`receivedsharecache.Cache.RemoveSpaceShares`). It can be scoped to a single | ||
| space and supports a dry-run mode. An entry is only removed when the share is | ||
| confirmed absent after a successful space listing, so live shares are never | ||
| removed on a transient read failure. | ||
|
|
||
| https://github.com/owncloud/reva/pull/656 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -278,6 +278,97 @@ func (c *Cache) Remove(ctx context.Context, userID, spaceID, shareID string) err | |
| return err | ||
| } | ||
|
|
||
| // RemoveSpaceShares removes multiple entries of a single space from the cache in | ||
| // one persist. It is the batched counterpart of Remove: collecting all shareIDs | ||
| // to drop for a user's space avoids the N serial read-modify-write cycles that a | ||
| // per-share Remove would incur. Passing an empty shareIDs slice is a no-op. | ||
| func (c *Cache) RemoveSpaceShares(ctx context.Context, userID, spaceID string, shareIDs []string) error { | ||
| ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "RemoveSpaceShares") | ||
| defer span.End() | ||
| span.SetAttributes(attribute.String("cs3.userid", userID), attribute.String("cs3.spaceid", spaceID)) | ||
|
|
||
| if len(shareIDs) == 0 { | ||
| return nil | ||
| } | ||
|
|
||
| unlock := c.lockUser(userID) | ||
| defer unlock() | ||
|
|
||
| persistFunc := func() error { | ||
| c.initializeIfNeeded(userID, spaceID) | ||
|
|
||
| rss, _ := c.ReceivedSpaces.Load(userID) | ||
| receivedSpace := rss.Spaces[spaceID] | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have guarantees that the |
||
| if receivedSpace == nil { | ||
| // initializeIfNeeded only creates the space entry for a non-empty | ||
| // spaceID; guard against a nil entry so we never dereference it. | ||
| return nil | ||
| } | ||
| if receivedSpace.States == nil { | ||
| receivedSpace.States = map[string]*State{} | ||
| } | ||
| for _, shareID := range shareIDs { | ||
| delete(receivedSpace.States, shareID) | ||
| } | ||
| if len(receivedSpace.States) == 0 { | ||
| delete(rss.Spaces, spaceID) | ||
| } | ||
|
|
||
| return c.persist(ctx, userID) | ||
| } | ||
|
|
||
| log := appctx.GetLogger(ctx).With(). | ||
| Str("hostname", os.Getenv("HOSTNAME")). | ||
| Str("userID", userID). | ||
| Str("spaceID", spaceID).Logger() | ||
|
|
||
| var err error | ||
| for attempt := 0; attempt < 20; attempt++ { | ||
| select { | ||
| case <-ctx.Done(): | ||
| return ctx.Err() | ||
| default: | ||
| } | ||
| err = persistFunc() | ||
| switch err.(type) { | ||
| case nil: | ||
| span.SetStatus(codes.Ok, "") | ||
| return nil | ||
| case errtypes.Aborted: | ||
| log.Debug().Msg("aborted when persisting removed received shares: etag changed. retrying...") | ||
| // this is the expected status code from the server when the if-match etag check fails | ||
| // continue with sync below | ||
| case errtypes.PreconditionFailed: | ||
| log.Debug().Msg("precondition failed when persisting removed received shares: etag changed. retrying...") | ||
| // actually, this is the wrong status code and we treat it like errtypes.Aborted because of inconsistencies on the server side | ||
| // continue with sync below | ||
| case errtypes.AlreadyExists: | ||
| log.Debug().Msg("already exists when persisting removed received shares. retrying...") | ||
| // CS3 uses an already exists error instead of precondition failed when using an If-None-Match=* header / IfExists flag in the InitiateFileUpload call. | ||
| // That happens when the cache thinks there is no file. | ||
| // continue with sync below | ||
| default: | ||
| span.SetStatus(codes.Error, fmt.Sprintf("persisting removed received shares failed. giving up: %s", err.Error())) | ||
| log.Error().Err(err).Msg("persisting removed received shares failed") | ||
| return err | ||
| } | ||
| timer := time.NewTimer(expBackoff(attempt)) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be easier to use |
||
| select { | ||
| case <-timer.C: | ||
| case <-ctx.Done(): | ||
| timer.Stop() | ||
| return ctx.Err() | ||
| } | ||
| if err := c.syncWithLock(ctx, userID); err != nil { | ||
| span.RecordError(err) | ||
| span.SetStatus(codes.Error, err.Error()) | ||
| log.Error().Err(err).Msg("persisting removed received shares failed. giving up.") | ||
| return err | ||
| } | ||
| } | ||
| return err | ||
| } | ||
|
|
||
| // List returns a list of received shares for a given user | ||
| // The return list is guaranteed to be thread-safe | ||
| func (c *Cache) List(ctx context.Context, userID string) (map[string]*Space, 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.
Any additional information we can extract from the errors? Right now, I don't think it matters that there have been 3 errors or 300, what matters is if there have been any error or none at all. A boolean type provides the same value.
Maybe including the first 25 or 50 error messages (if there are more) might be interesting. We don't want to have all the error messages, but some of them might be useful to have an idea of what's going on.