github.com/cs3org/reva/v2@v2.27.7/pkg/share/manager/jsoncs3/sharecache/sharecache.go (about)

     1  // Copyright 2018-2021 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 sharecache
    20  
    21  import (
    22  	"context"
    23  	"encoding/json"
    24  	"fmt"
    25  	"os"
    26  	"path"
    27  	"path/filepath"
    28  	"sync"
    29  	"time"
    30  
    31  	"go.opentelemetry.io/otel/attribute"
    32  	"go.opentelemetry.io/otel/codes"
    33  	"golang.org/x/exp/maps"
    34  
    35  	"github.com/cs3org/reva/v2/pkg/appctx"
    36  	"github.com/cs3org/reva/v2/pkg/errtypes"
    37  	"github.com/cs3org/reva/v2/pkg/share/manager/jsoncs3/shareid"
    38  	"github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/mtimesyncedcache"
    39  	"github.com/cs3org/reva/v2/pkg/storage/utils/metadata"
    40  )
    41  
    42  // name is the Tracer name used to identify this instrumentation library.
    43  const tracerName = "sharecache"
    44  
    45  // Cache caches the list of share ids for users/groups
    46  // It functions as an in-memory cache with a persistence layer
    47  // The storage is sharded by user/group
    48  type Cache struct {
    49  	lockMap sync.Map
    50  
    51  	UserShares mtimesyncedcache.Map[string, *UserShareCache]
    52  
    53  	storage   metadata.Storage
    54  	namespace string
    55  	filename  string
    56  	ttl       time.Duration
    57  }
    58  
    59  // UserShareCache holds the space/share map for one user
    60  type UserShareCache struct {
    61  	UserShares map[string]*SpaceShareIDs
    62  
    63  	Etag string
    64  }
    65  
    66  // SpaceShareIDs holds the unique list of share ids for a space
    67  type SpaceShareIDs struct {
    68  	IDs map[string]struct{}
    69  }
    70  
    71  func (c *Cache) lockUser(userID string) func() {
    72  	v, _ := c.lockMap.LoadOrStore(userID, &sync.Mutex{})
    73  	lock := v.(*sync.Mutex)
    74  
    75  	lock.Lock()
    76  	return func() { lock.Unlock() }
    77  }
    78  
    79  // New returns a new Cache instance
    80  func New(s metadata.Storage, namespace, filename string, ttl time.Duration) Cache {
    81  	return Cache{
    82  		UserShares: mtimesyncedcache.Map[string, *UserShareCache]{},
    83  		storage:    s,
    84  		namespace:  namespace,
    85  		filename:   filename,
    86  		ttl:        ttl,
    87  		lockMap:    sync.Map{},
    88  	}
    89  }
    90  
    91  // Add adds a share to the cache
    92  func (c *Cache) Add(ctx context.Context, userid, shareID string) error {
    93  	ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Grab lock")
    94  	unlock := c.lockUser(userid)
    95  	span.End()
    96  	span.SetAttributes(attribute.String("cs3.userid", userid))
    97  	defer unlock()
    98  
    99  	if _, ok := c.UserShares.Load(userid); !ok {
   100  		err := c.syncWithLock(ctx, userid)
   101  		if err != nil {
   102  			return err
   103  		}
   104  	}
   105  
   106  	ctx, span = appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Add")
   107  	defer span.End()
   108  	span.SetAttributes(attribute.String("cs3.userid", userid), attribute.String("cs3.shareid", shareID))
   109  
   110  	storageid, spaceid, _ := shareid.Decode(shareID)
   111  	ssid := storageid + shareid.IDDelimiter + spaceid
   112  
   113  	persistFunc := func() error {
   114  		c.initializeIfNeeded(userid, ssid)
   115  
   116  		// add share id
   117  		us, _ := c.UserShares.Load(userid)
   118  		us.UserShares[ssid].IDs[shareID] = struct{}{}
   119  		return c.Persist(ctx, userid)
   120  	}
   121  
   122  	log := appctx.GetLogger(ctx).With().
   123  		Str("hostname", os.Getenv("HOSTNAME")).
   124  		Str("userID", userid).
   125  		Str("shareID", shareID).Logger()
   126  
   127  	var err error
   128  	for retries := 100; retries > 0; retries-- {
   129  		err = persistFunc()
   130  		switch err.(type) {
   131  		case nil:
   132  			span.SetStatus(codes.Ok, "")
   133  			return nil
   134  		case errtypes.Aborted:
   135  			log.Debug().Msg("aborted when persisting added share: etag changed. retrying...")
   136  			// this is the expected status code from the server when the if-match etag check fails
   137  			// continue with sync below
   138  		case errtypes.PreconditionFailed:
   139  			log.Debug().Msg("precondition failed when persisting added share: etag changed. retrying...")
   140  			// actually, this is the wrong status code and we treat it like errtypes.Aborted because of inconsistencies on the server side
   141  			// continue with sync below
   142  		case errtypes.AlreadyExists:
   143  			log.Debug().Msg("already exists when persisting added share. retrying...")
   144  			// CS3 uses an already exists error instead of precondition failed when using an If-None-Match=* header / IfExists flag in the InitiateFileUpload call.
   145  			// Thas happens when the cache thinks there is no file.
   146  			// continue with sync below
   147  		default:
   148  			span.SetStatus(codes.Error, fmt.Sprintf("persisting added share failed. giving up: %s", err.Error()))
   149  			log.Error().Err(err).Msg("persisting added share failed")
   150  			return err
   151  		}
   152  		if err := c.syncWithLock(ctx, userid); err != nil {
   153  			span.RecordError(err)
   154  			span.SetStatus(codes.Error, err.Error())
   155  			log.Error().Err(err).Msg("persisting added share failed. giving up.")
   156  			return err
   157  		}
   158  	}
   159  	return err
   160  }
   161  
   162  // Remove removes a share for the given user
   163  func (c *Cache) Remove(ctx context.Context, userid, shareID string) error {
   164  	ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Grab lock")
   165  	unlock := c.lockUser(userid)
   166  	span.End()
   167  	span.SetAttributes(attribute.String("cs3.userid", userid))
   168  	defer unlock()
   169  
   170  	if _, ok := c.UserShares.Load(userid); ok {
   171  		err := c.syncWithLock(ctx, userid)
   172  		if err != nil {
   173  			return err
   174  		}
   175  	}
   176  
   177  	ctx, span = appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Remove")
   178  	defer span.End()
   179  	span.SetAttributes(attribute.String("cs3.userid", userid), attribute.String("cs3.shareid", shareID))
   180  
   181  	storageid, spaceid, _ := shareid.Decode(shareID)
   182  	ssid := storageid + shareid.IDDelimiter + spaceid
   183  
   184  	persistFunc := func() error {
   185  		us, loaded := c.UserShares.LoadOrStore(userid, &UserShareCache{
   186  			UserShares: map[string]*SpaceShareIDs{},
   187  		})
   188  
   189  		if loaded {
   190  			// remove share id
   191  			delete(us.UserShares[ssid].IDs, shareID)
   192  		}
   193  
   194  		return c.Persist(ctx, userid)
   195  	}
   196  
   197  	log := appctx.GetLogger(ctx).With().
   198  		Str("hostname", os.Getenv("HOSTNAME")).
   199  		Str("userID", userid).
   200  		Str("shareID", shareID).Logger()
   201  
   202  	var err error
   203  	for retries := 100; retries > 0; retries-- {
   204  		err = persistFunc()
   205  		switch err.(type) {
   206  		case nil:
   207  			span.SetStatus(codes.Ok, "")
   208  			return nil
   209  		case errtypes.Aborted:
   210  			log.Debug().Msg("aborted when persisting removed share: etag changed. retrying...")
   211  			// this is the expected status code from the server when the if-match etag check fails
   212  			// continue with sync below
   213  		case errtypes.PreconditionFailed:
   214  			log.Debug().Msg("precondition failed when persisting removed share: etag changed. retrying...")
   215  			// actually, this is the wrong status code and we treat it like errtypes.Aborted because of inconsistencies on the server side
   216  			// continue with sync below
   217  		case errtypes.AlreadyExists:
   218  			log.Debug().Msg("file already existed when persisting removed share. retrying...")
   219  			// CS3 uses an already exists error instead of precondition failed when using an If-None-Match=* header / IfExists flag in the InitiateFileUpload call.
   220  			// Thas happens when the cache thinks there is no file.
   221  			// continue with sync below
   222  		default:
   223  			span.SetStatus(codes.Error, fmt.Sprintf("persisting removed share failed. giving up: %s", err.Error()))
   224  			log.Error().Err(err).Msg("persisting removed share failed")
   225  			return err
   226  		}
   227  		if err := c.syncWithLock(ctx, userid); err != nil {
   228  			span.RecordError(err)
   229  			span.SetStatus(codes.Error, err.Error())
   230  			return err
   231  		}
   232  	}
   233  
   234  	return err
   235  }
   236  
   237  // List return the list of spaces/shares for the given user/group
   238  func (c *Cache) List(ctx context.Context, userid string) (map[string]SpaceShareIDs, error) {
   239  	ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Grab lock")
   240  	unlock := c.lockUser(userid)
   241  	span.End()
   242  	span.SetAttributes(attribute.String("cs3.userid", userid))
   243  	defer unlock()
   244  	if err := c.syncWithLock(ctx, userid); err != nil {
   245  		return nil, err
   246  	}
   247  
   248  	r := map[string]SpaceShareIDs{}
   249  	us, ok := c.UserShares.Load(userid)
   250  	if !ok {
   251  		return r, nil
   252  	}
   253  
   254  	for ssid, cached := range us.UserShares {
   255  		r[ssid] = SpaceShareIDs{
   256  			IDs: maps.Clone(cached.IDs),
   257  		}
   258  	}
   259  	return r, nil
   260  }
   261  
   262  func (c *Cache) syncWithLock(ctx context.Context, userID string) error {
   263  	ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Sync")
   264  	defer span.End()
   265  	span.SetAttributes(attribute.String("cs3.userid", userID))
   266  
   267  	log := appctx.GetLogger(ctx).With().Str("userID", userID).Logger()
   268  
   269  	c.initializeIfNeeded(userID, "")
   270  
   271  	userCreatedPath := c.userCreatedPath(userID)
   272  	span.AddEvent("updating cache")
   273  	//  - update cached list of created shares for the user in memory if changed
   274  	dlreq := metadata.DownloadRequest{
   275  		Path: userCreatedPath,
   276  	}
   277  	if us, ok := c.UserShares.Load(userID); ok && us.Etag != "" {
   278  		dlreq.IfNoneMatch = []string{us.Etag}
   279  	}
   280  
   281  	dlres, err := c.storage.Download(ctx, dlreq)
   282  	switch err.(type) {
   283  	case nil:
   284  		span.AddEvent("updating local cache")
   285  	case errtypes.NotFound:
   286  		span.SetStatus(codes.Ok, "")
   287  		return nil
   288  	case errtypes.NotModified:
   289  		span.SetStatus(codes.Ok, "")
   290  		return nil
   291  	default:
   292  		span.SetStatus(codes.Error, fmt.Sprintf("Failed to download the share cache: %s", err.Error()))
   293  		log.Error().Err(err).Msg("Failed to download the share cache")
   294  		return err
   295  	}
   296  
   297  	newShareCache := &UserShareCache{}
   298  	err = json.Unmarshal(dlres.Content, newShareCache)
   299  	if err != nil {
   300  		span.SetStatus(codes.Error, fmt.Sprintf("Failed to unmarshal the share cache: %s", err.Error()))
   301  		log.Error().Err(err).Msg("Failed to unmarshal the share cache")
   302  		return err
   303  	}
   304  	newShareCache.Etag = dlres.Etag
   305  
   306  	c.UserShares.Store(userID, newShareCache)
   307  	span.SetStatus(codes.Ok, "")
   308  	return nil
   309  }
   310  
   311  // Persist persists the data for one user/group to the storage
   312  func (c *Cache) Persist(ctx context.Context, userid string) error {
   313  	ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Persist")
   314  	defer span.End()
   315  	span.SetAttributes(attribute.String("cs3.userid", userid))
   316  
   317  	us, ok := c.UserShares.Load(userid)
   318  	if !ok {
   319  		span.SetStatus(codes.Ok, "no user shares")
   320  		return nil
   321  	}
   322  	createdBytes, err := json.Marshal(us)
   323  	if err != nil {
   324  		span.RecordError(err)
   325  		span.SetStatus(codes.Error, err.Error())
   326  		return err
   327  	}
   328  	jsonPath := c.userCreatedPath(userid)
   329  	if err := c.storage.MakeDirIfNotExist(ctx, path.Dir(jsonPath)); err != nil {
   330  		span.RecordError(err)
   331  		span.SetStatus(codes.Error, err.Error())
   332  		return err
   333  	}
   334  
   335  	ur := metadata.UploadRequest{
   336  		Path:        jsonPath,
   337  		Content:     createdBytes,
   338  		IfMatchEtag: us.Etag,
   339  	}
   340  	// 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
   341  	// > If the field value is "*", the condition is false if the origin server has a current representation for the target resource.
   342  	if us.Etag == "" {
   343  		ur.IfNoneMatch = []string{"*"}
   344  	}
   345  
   346  	res, err := c.storage.Upload(ctx, ur)
   347  	if err != nil {
   348  		span.RecordError(err)
   349  		span.SetStatus(codes.Error, err.Error())
   350  		return err
   351  	}
   352  	us.Etag = res.Etag
   353  
   354  	span.SetStatus(codes.Ok, "")
   355  	return nil
   356  }
   357  
   358  func (c *Cache) userCreatedPath(userid string) string {
   359  	return filepath.Join("/", c.namespace, userid, c.filename)
   360  }
   361  
   362  func (c *Cache) initializeIfNeeded(userid, ssid string) {
   363  	us, _ := c.UserShares.LoadOrStore(userid, &UserShareCache{
   364  		UserShares: map[string]*SpaceShareIDs{},
   365  	})
   366  	if ssid != "" && us.UserShares[ssid] == nil {
   367  		us.UserShares[ssid] = &SpaceShareIDs{
   368  			IDs: map[string]struct{}{},
   369  		}
   370  	}
   371  }