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  }