github.com/cs3org/reva/v2@v2.27.7/pkg/share/manager/jsoncs3/receivedsharecache/receivedsharecache.go (about) 1 // Copyright 2018-2022 CERN 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 // 15 // In applying this license, CERN does not waive the privileges and immunities 16 // granted to it by virtue of its status as an Intergovernmental Organization 17 // or submit itself to any jurisdiction. 18 19 package receivedsharecache 20 21 import ( 22 "context" 23 "encoding/json" 24 "fmt" 25 "os" 26 "path" 27 "path/filepath" 28 "sync" 29 "time" 30 31 collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" 32 provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" 33 "github.com/cs3org/reva/v2/pkg/appctx" 34 "github.com/cs3org/reva/v2/pkg/errtypes" 35 "github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/mtimesyncedcache" 36 "github.com/cs3org/reva/v2/pkg/storage/utils/metadata" 37 "go.opentelemetry.io/otel/attribute" 38 "go.opentelemetry.io/otel/codes" 39 ) 40 41 // name is the Tracer name used to identify this instrumentation library. 42 const tracerName = "receivedsharecache" 43 44 // Cache stores the list of received shares and their states 45 // It functions as an in-memory cache with a persistence layer 46 // The storage is sharded by user 47 type Cache struct { 48 lockMap sync.Map 49 50 ReceivedSpaces mtimesyncedcache.Map[string, *Spaces] 51 52 storage metadata.Storage 53 ttl time.Duration 54 } 55 56 // Spaces holds the received shares of one user per space 57 type Spaces struct { 58 Spaces map[string]*Space 59 60 etag string 61 } 62 63 // Space holds the received shares of one user in one space 64 type Space struct { 65 States map[string]*State 66 } 67 68 // State holds the state information of a received share 69 type State struct { 70 State collaboration.ShareState 71 MountPoint *provider.Reference 72 Hidden bool 73 } 74 75 // New returns a new Cache instance 76 func New(s metadata.Storage, ttl time.Duration) Cache { 77 return Cache{ 78 ReceivedSpaces: mtimesyncedcache.Map[string, *Spaces]{}, 79 storage: s, 80 ttl: ttl, 81 lockMap: sync.Map{}, 82 } 83 } 84 85 func (c *Cache) lockUser(userID string) func() { 86 v, _ := c.lockMap.LoadOrStore(userID, &sync.Mutex{}) 87 lock := v.(*sync.Mutex) 88 89 lock.Lock() 90 return func() { lock.Unlock() } 91 } 92 93 // Add adds a new entry to the cache 94 func (c *Cache) Add(ctx context.Context, userID, spaceID string, rs *collaboration.ReceivedShare) error { 95 ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Grab lock") 96 unlock := c.lockUser(userID) 97 span.End() 98 span.SetAttributes(attribute.String("cs3.userid", userID)) 99 defer unlock() 100 101 if _, ok := c.ReceivedSpaces.Load(userID); !ok { 102 err := c.syncWithLock(ctx, userID) 103 if err != nil { 104 return err 105 } 106 } 107 108 ctx, span = appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Add") 109 defer span.End() 110 span.SetAttributes(attribute.String("cs3.userid", userID), attribute.String("cs3.spaceid", spaceID)) 111 112 persistFunc := func() error { 113 c.initializeIfNeeded(userID, spaceID) 114 115 rss, _ := c.ReceivedSpaces.Load(userID) 116 receivedSpace := rss.Spaces[spaceID] 117 if receivedSpace.States == nil { 118 receivedSpace.States = map[string]*State{} 119 } 120 receivedSpace.States[rs.Share.Id.GetOpaqueId()] = &State{ 121 State: rs.State, 122 MountPoint: rs.MountPoint, 123 Hidden: rs.Hidden, 124 } 125 126 return c.persist(ctx, userID) 127 } 128 129 log := appctx.GetLogger(ctx).With(). 130 Str("hostname", os.Getenv("HOSTNAME")). 131 Str("userID", userID). 132 Str("spaceID", spaceID).Logger() 133 134 var err error 135 for retries := 100; retries > 0; retries-- { 136 err = persistFunc() 137 switch err.(type) { 138 case nil: 139 span.SetStatus(codes.Ok, "") 140 return nil 141 case errtypes.Aborted: 142 log.Debug().Msg("aborted when persisting added received share: etag changed. retrying...") 143 // this is the expected status code from the server when the if-match etag check fails 144 // continue with sync below 145 case errtypes.PreconditionFailed: 146 log.Debug().Msg("precondition failed when persisting added received share: etag changed. retrying...") 147 // actually, this is the wrong status code and we treat it like errtypes.Aborted because of inconsistencies on the server side 148 // continue with sync below 149 case errtypes.AlreadyExists: 150 log.Debug().Msg("already exists when persisting added received share. retrying...") 151 // CS3 uses an already exists error instead of precondition failed when using an If-None-Match=* header / IfExists flag in the InitiateFileUpload call. 152 // Thas happens when the cache thinks there is no file. 153 // continue with sync below 154 default: 155 span.SetStatus(codes.Error, fmt.Sprintf("persisting added received share failed. giving up: %s", err.Error())) 156 log.Error().Err(err).Msg("persisting added received share failed") 157 return err 158 } 159 if err := c.syncWithLock(ctx, userID); err != nil { 160 span.RecordError(err) 161 span.SetStatus(codes.Error, err.Error()) 162 log.Error().Err(err).Msg("persisting added received share failed. giving up.") 163 return err 164 } 165 } 166 return err 167 } 168 169 // Get returns one entry from the cache 170 func (c *Cache) Get(ctx context.Context, userID, spaceID, shareID string) (*State, error) { 171 ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Grab lock") 172 unlock := c.lockUser(userID) 173 span.End() 174 span.SetAttributes(attribute.String("cs3.userid", userID)) 175 defer unlock() 176 177 err := c.syncWithLock(ctx, userID) 178 if err != nil { 179 return nil, err 180 } 181 rss, ok := c.ReceivedSpaces.Load(userID) 182 if !ok || rss.Spaces[spaceID] == nil { 183 return nil, nil 184 } 185 return rss.Spaces[spaceID].States[shareID], nil 186 } 187 188 // Remove removes an entry from the cache 189 func (c *Cache) Remove(ctx context.Context, userID, spaceID, shareID string) error { 190 ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Grab lock") 191 unlock := c.lockUser(userID) 192 span.End() 193 span.SetAttributes(attribute.String("cs3.userid", userID)) 194 defer unlock() 195 196 ctx, span = appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Add") 197 defer span.End() 198 span.SetAttributes(attribute.String("cs3.userid", userID), attribute.String("cs3.spaceid", spaceID)) 199 200 persistFunc := func() error { 201 c.initializeIfNeeded(userID, spaceID) 202 203 rss, _ := c.ReceivedSpaces.Load(userID) 204 receivedSpace := rss.Spaces[spaceID] 205 if receivedSpace.States == nil { 206 receivedSpace.States = map[string]*State{} 207 } 208 delete(receivedSpace.States, shareID) 209 if len(receivedSpace.States) == 0 { 210 delete(rss.Spaces, spaceID) 211 } 212 213 return c.persist(ctx, userID) 214 } 215 216 log := appctx.GetLogger(ctx).With(). 217 Str("hostname", os.Getenv("HOSTNAME")). 218 Str("userID", userID). 219 Str("spaceID", spaceID).Logger() 220 221 var err error 222 for retries := 100; retries > 0; retries-- { 223 err = persistFunc() 224 switch err.(type) { 225 case nil: 226 span.SetStatus(codes.Ok, "") 227 return nil 228 case errtypes.Aborted: 229 log.Debug().Msg("aborted when persisting added received share: etag changed. retrying...") 230 // this is the expected status code from the server when the if-match etag check fails 231 // continue with sync below 232 case errtypes.PreconditionFailed: 233 log.Debug().Msg("precondition failed when persisting added received share: etag changed. retrying...") 234 // actually, this is the wrong status code and we treat it like errtypes.Aborted because of inconsistencies on the server side 235 // continue with sync below 236 case errtypes.AlreadyExists: 237 log.Debug().Msg("already exists when persisting added received share. retrying...") 238 // CS3 uses an already exists error instead of precondition failed when using an If-None-Match=* header / IfExists flag in the InitiateFileUpload call. 239 // Thas happens when the cache thinks there is no file. 240 // continue with sync below 241 default: 242 span.SetStatus(codes.Error, fmt.Sprintf("persisting added received share failed. giving up: %s", err.Error())) 243 log.Error().Err(err).Msg("persisting added received share failed") 244 return err 245 } 246 if err := c.syncWithLock(ctx, userID); err != nil { 247 span.RecordError(err) 248 span.SetStatus(codes.Error, err.Error()) 249 log.Error().Err(err).Msg("persisting added received share failed. giving up.") 250 return err 251 } 252 } 253 return err 254 } 255 256 // List returns a list of received shares for a given user 257 // The return list is guaranteed to be thread-safe 258 func (c *Cache) List(ctx context.Context, userID string) (map[string]*Space, error) { 259 ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Grab lock") 260 unlock := c.lockUser(userID) 261 span.End() 262 span.SetAttributes(attribute.String("cs3.userid", userID)) 263 defer unlock() 264 265 err := c.syncWithLock(ctx, userID) 266 if err != nil { 267 return nil, err 268 } 269 270 spaces := map[string]*Space{} 271 rss, _ := c.ReceivedSpaces.Load(userID) 272 for spaceID, space := range rss.Spaces { 273 spaceCopy := &Space{ 274 States: map[string]*State{}, 275 } 276 for shareID, state := range space.States { 277 spaceCopy.States[shareID] = &State{ 278 State: state.State, 279 MountPoint: state.MountPoint, 280 Hidden: state.Hidden, 281 } 282 } 283 spaces[spaceID] = spaceCopy 284 } 285 return spaces, nil 286 } 287 288 func (c *Cache) syncWithLock(ctx context.Context, userID string) error { 289 ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Sync") 290 defer span.End() 291 span.SetAttributes(attribute.String("cs3.userid", userID)) 292 293 log := appctx.GetLogger(ctx).With().Str("userID", userID).Logger() 294 295 c.initializeIfNeeded(userID, "") 296 297 jsonPath := userJSONPath(userID) 298 span.AddEvent("updating cache") 299 // - update cached list of created shares for the user in memory if changed 300 rss, _ := c.ReceivedSpaces.Load(userID) 301 dlres, err := c.storage.Download(ctx, metadata.DownloadRequest{ 302 Path: jsonPath, 303 IfNoneMatch: []string{rss.etag}, 304 }) 305 switch err.(type) { 306 case nil: 307 span.AddEvent("updating local cache") 308 case errtypes.NotFound: 309 span.SetStatus(codes.Ok, "") 310 return nil 311 case errtypes.NotModified: 312 span.SetStatus(codes.Ok, "") 313 return nil 314 default: 315 span.SetStatus(codes.Error, fmt.Sprintf("Failed to download the received share: %s", err.Error())) 316 log.Error().Err(err).Msg("Failed to download the received share") 317 return err 318 } 319 320 newSpaces := &Spaces{} 321 err = json.Unmarshal(dlres.Content, newSpaces) 322 if err != nil { 323 span.SetStatus(codes.Error, fmt.Sprintf("Failed to unmarshal the received share: %s", err.Error())) 324 log.Error().Err(err).Msg("Failed to unmarshal the received share") 325 return err 326 } 327 newSpaces.etag = dlres.Etag 328 329 c.ReceivedSpaces.Store(userID, newSpaces) 330 span.SetStatus(codes.Ok, "") 331 return nil 332 } 333 334 // persist persists the data for one user to the storage 335 func (c *Cache) persist(ctx context.Context, userID string) error { 336 ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Persist") 337 defer span.End() 338 span.SetAttributes(attribute.String("cs3.userid", userID)) 339 340 rss, ok := c.ReceivedSpaces.Load(userID) 341 if !ok { 342 span.SetStatus(codes.Ok, "no received shares") 343 return nil 344 } 345 346 createdBytes, err := json.Marshal(rss) 347 if err != nil { 348 span.RecordError(err) 349 span.SetStatus(codes.Error, err.Error()) 350 return err 351 } 352 jsonPath := userJSONPath(userID) 353 if err := c.storage.MakeDirIfNotExist(ctx, path.Dir(jsonPath)); err != nil { 354 span.RecordError(err) 355 span.SetStatus(codes.Error, err.Error()) 356 return err 357 } 358 359 ur := metadata.UploadRequest{ 360 Path: jsonPath, 361 Content: createdBytes, 362 IfMatchEtag: rss.etag, 363 } 364 // when there is no etag in memory make sure the file has not been created on the server, see https://www.rfc-editor.org/rfc/rfc9110#field.if-match 365 // > If the field value is "*", the condition is false if the origin server has a current representation for the target resource. 366 if rss.etag == "" { 367 ur.IfNoneMatch = []string{"*"} 368 } 369 370 res, err := c.storage.Upload(ctx, ur) 371 if err != nil { 372 span.RecordError(err) 373 span.SetStatus(codes.Error, err.Error()) 374 return err 375 } 376 rss.etag = res.Etag 377 378 span.SetStatus(codes.Ok, "") 379 return nil 380 } 381 382 func userJSONPath(userID string) string { 383 return filepath.Join("/users", userID, "received.json") 384 } 385 386 func (c *Cache) initializeIfNeeded(userID, spaceID string) { 387 rss, _ := c.ReceivedSpaces.LoadOrStore(userID, &Spaces{Spaces: map[string]*Space{}}) 388 if spaceID != "" && rss.Spaces[spaceID] == nil { 389 rss.Spaces[spaceID] = &Space{} 390 c.ReceivedSpaces.Store(userID, rss) 391 } 392 }