github.com/cs3org/reva/v2@v2.27.7/pkg/share/manager/jsoncs3/providercache/providercache.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 providercache
    20  
    21  import (
    22  	"context"
    23  	"encoding/json"
    24  	"fmt"
    25  	"os"
    26  	"path"
    27  	"path/filepath"
    28  	"strings"
    29  	"sync"
    30  	"time"
    31  
    32  	collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
    33  	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
    34  	"github.com/cs3org/reva/v2/pkg/appctx"
    35  	"github.com/cs3org/reva/v2/pkg/errtypes"
    36  	"github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/mtimesyncedcache"
    37  	"github.com/cs3org/reva/v2/pkg/storage/utils/metadata"
    38  	"go.opentelemetry.io/otel"
    39  	"go.opentelemetry.io/otel/attribute"
    40  	"go.opentelemetry.io/otel/codes"
    41  	"go.opentelemetry.io/otel/trace"
    42  	"golang.org/x/exp/maps"
    43  )
    44  
    45  var tracer trace.Tracer
    46  
    47  func init() {
    48  	tracer = otel.Tracer("github.com/cs3org/reva/v2/pkg/share/manager/jsoncs3/providercache")
    49  }
    50  
    51  // Cache holds share information structured by provider and space
    52  type Cache struct {
    53  	lockMap sync.Map
    54  
    55  	Providers mtimesyncedcache.Map[string, *Spaces]
    56  
    57  	storage metadata.Storage
    58  	ttl     time.Duration
    59  }
    60  
    61  // Spaces holds the share information for provider
    62  type Spaces struct {
    63  	Spaces mtimesyncedcache.Map[string, *Shares]
    64  }
    65  
    66  // Shares holds the share information of one space
    67  type Shares struct {
    68  	Shares map[string]*collaboration.Share
    69  
    70  	Etag string
    71  }
    72  
    73  // UnmarshalJSON overrides the default unmarshaling
    74  // Shares are tricky to unmarshal because they contain an interface (Grantee) which makes the json Unmarshal bail out
    75  // To work around that problem we unmarshal into json.RawMessage in a first step and then try to manually unmarshal
    76  // into the specific types in a second step.
    77  func (s *Shares) UnmarshalJSON(data []byte) error {
    78  	tmp := struct {
    79  		Shares map[string]json.RawMessage
    80  	}{}
    81  
    82  	err := json.Unmarshal(data, &tmp)
    83  	if err != nil {
    84  		return err
    85  	}
    86  
    87  	s.Shares = make(map[string]*collaboration.Share, len(tmp.Shares))
    88  	for id, genericShare := range tmp.Shares {
    89  		userShare := &collaboration.Share{
    90  			Grantee: &provider.Grantee{Id: &provider.Grantee_UserId{}},
    91  		}
    92  		err = json.Unmarshal(genericShare, userShare) // is this a user share?
    93  		if err == nil && userShare.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_USER {
    94  			s.Shares[id] = userShare
    95  			continue
    96  		}
    97  
    98  		groupShare := &collaboration.Share{
    99  			Grantee: &provider.Grantee{Id: &provider.Grantee_GroupId{}},
   100  		}
   101  		err = json.Unmarshal(genericShare, groupShare) // is this a group share?
   102  		if err == nil && groupShare.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_GROUP {
   103  			s.Shares[id] = groupShare
   104  			continue
   105  		}
   106  
   107  		invalidShare := &collaboration.Share{}
   108  		err = json.Unmarshal(genericShare, invalidShare) // invalid
   109  		if err == nil {
   110  			s.Shares[id] = invalidShare
   111  			continue
   112  		}
   113  
   114  		return err
   115  	}
   116  
   117  	return nil
   118  }
   119  
   120  // LockSpace locks the cache for a given space and returns an unlock function
   121  func (c *Cache) LockSpace(spaceID string) func() {
   122  	v, _ := c.lockMap.LoadOrStore(spaceID, &sync.Mutex{})
   123  	lock := v.(*sync.Mutex)
   124  
   125  	lock.Lock()
   126  	return func() { lock.Unlock() }
   127  }
   128  
   129  // New returns a new Cache instance
   130  func New(s metadata.Storage, ttl time.Duration) Cache {
   131  	return Cache{
   132  		Providers: mtimesyncedcache.Map[string, *Spaces]{},
   133  		storage:   s,
   134  		ttl:       ttl,
   135  		lockMap:   sync.Map{},
   136  	}
   137  }
   138  
   139  func (c *Cache) isSpaceCached(storageID, spaceID string) bool {
   140  	spaces, ok := c.Providers.Load(storageID)
   141  	if !ok {
   142  		return false
   143  	}
   144  	_, ok = spaces.Spaces.Load(spaceID)
   145  	return ok
   146  }
   147  
   148  // Add adds a share to the cache
   149  func (c *Cache) Add(ctx context.Context, storageID, spaceID, shareID string, share *collaboration.Share) error {
   150  	ctx, span := tracer.Start(ctx, "Add")
   151  	defer span.End()
   152  	span.SetAttributes(attribute.String("cs3.storageid", storageID), attribute.String("cs3.spaceid", spaceID), attribute.String("cs3.shareid", shareID))
   153  
   154  	switch {
   155  	case storageID == "":
   156  		return fmt.Errorf("missing storage id")
   157  	case spaceID == "":
   158  		return fmt.Errorf("missing space id")
   159  	case shareID == "":
   160  		return fmt.Errorf("missing share id")
   161  	}
   162  
   163  	unlock := c.LockSpace(spaceID)
   164  	defer unlock()
   165  	span.AddEvent("got lock")
   166  
   167  	var err error
   168  	if !c.isSpaceCached(storageID, spaceID) {
   169  		err = c.syncWithLock(ctx, storageID, spaceID)
   170  		if err != nil {
   171  			return err
   172  		}
   173  	}
   174  
   175  	log := appctx.GetLogger(ctx).With().
   176  		Str("hostname", os.Getenv("HOSTNAME")).
   177  		Str("storageID", storageID).
   178  		Str("spaceID", spaceID).
   179  		Str("shareID", shareID).Logger()
   180  
   181  	persistFunc := func() error {
   182  
   183  		spaces, _ := c.Providers.Load(storageID)
   184  		space, _ := spaces.Spaces.Load(spaceID)
   185  
   186  		log.Info().Interface("shares", maps.Keys(space.Shares)).Str("New share", shareID).Msg("Adding share to space")
   187  		space.Shares[shareID] = share
   188  
   189  		return c.Persist(ctx, storageID, spaceID)
   190  	}
   191  
   192  	for retries := 100; retries > 0; retries-- {
   193  		err = persistFunc()
   194  		switch err.(type) {
   195  		case nil:
   196  			span.SetStatus(codes.Ok, "")
   197  			return nil
   198  		case errtypes.Aborted:
   199  			log.Debug().Msg("aborted when persisting added provider share: etag changed. retrying...")
   200  			// this is the expected status code from the server when the if-match etag check fails
   201  			// continue with sync below
   202  		case errtypes.PreconditionFailed:
   203  			log.Debug().Msg("precondition failed when persisting added provider share: etag changed. retrying...")
   204  			// actually, this is the wrong status code and we treat it like errtypes.Aborted because of inconsistencies on the server side
   205  			// continue with sync below
   206  		case errtypes.AlreadyExists:
   207  			log.Debug().Msg("already exists when persisting added provider share. retrying...")
   208  			// CS3 uses an already exists error instead of precondition failed when using an If-None-Match=* header / IfExists flag in the InitiateFileUpload call.
   209  			// Thas happens when the cache thinks there is no file.
   210  			// continue with sync below
   211  		default:
   212  			span.SetStatus(codes.Error, fmt.Sprintf("persisting added provider share failed. giving up: %s", err.Error()))
   213  			log.Error().Err(err).Msg("persisting added provider share failed")
   214  			return err
   215  		}
   216  		if err := c.syncWithLock(ctx, storageID, spaceID); err != nil {
   217  			span.RecordError(err)
   218  			span.SetStatus(codes.Error, err.Error())
   219  			log.Error().Err(err).Msg("persisting added provider share failed. giving up.")
   220  			return err
   221  		}
   222  	}
   223  
   224  	return err
   225  }
   226  
   227  // Remove removes a share from the cache
   228  func (c *Cache) Remove(ctx context.Context, storageID, spaceID, shareID string) error {
   229  	ctx, span := tracer.Start(ctx, "Remove")
   230  	defer span.End()
   231  	span.SetAttributes(attribute.String("cs3.storageid", storageID), attribute.String("cs3.spaceid", spaceID), attribute.String("cs3.shareid", shareID))
   232  
   233  	unlock := c.LockSpace(spaceID)
   234  	defer unlock()
   235  	span.AddEvent("got lock")
   236  
   237  	if !c.isSpaceCached(storageID, spaceID) {
   238  		err := c.syncWithLock(ctx, storageID, spaceID)
   239  		if err != nil {
   240  			return err
   241  		}
   242  	}
   243  
   244  	persistFunc := func() error {
   245  		spaces, ok := c.Providers.Load(storageID)
   246  		if !ok {
   247  			return nil
   248  		}
   249  		space, _ := spaces.Spaces.Load(spaceID)
   250  		if !ok {
   251  			return nil
   252  		}
   253  		delete(space.Shares, shareID)
   254  
   255  		return c.Persist(ctx, storageID, spaceID)
   256  	}
   257  
   258  	log := appctx.GetLogger(ctx).With().
   259  		Str("hostname", os.Getenv("HOSTNAME")).
   260  		Str("storageID", storageID).
   261  		Str("spaceID", spaceID).
   262  		Str("shareID", shareID).Logger()
   263  
   264  	var err error
   265  	for retries := 100; retries > 0; retries-- {
   266  		err = persistFunc()
   267  		switch err.(type) {
   268  		case nil:
   269  			span.SetStatus(codes.Ok, "")
   270  			return nil
   271  		case errtypes.Aborted:
   272  			log.Debug().Msg("aborted when persisting removed provider share: etag changed. retrying...")
   273  			// this is the expected status code from the server when the if-match etag check fails
   274  			// continue with sync below
   275  		case errtypes.PreconditionFailed:
   276  			log.Debug().Msg("precondition failed when persisting removed provider share: etag changed. retrying...")
   277  			// actually, this is the wrong status code and we treat it like errtypes.Aborted because of inconsistencies on the server side
   278  			// continue with sync below
   279  		default:
   280  			span.SetStatus(codes.Error, fmt.Sprintf("persisting removed provider share failed. giving up: %s", err.Error()))
   281  			log.Error().Err(err).Msg("persisting removed provider share failed")
   282  			return err
   283  		}
   284  		if err := c.syncWithLock(ctx, storageID, spaceID); err != nil {
   285  			span.RecordError(err)
   286  			span.SetStatus(codes.Error, err.Error())
   287  			log.Error().Err(err).Msg("persisting removed provider share failed. giving up.")
   288  			return err
   289  		}
   290  	}
   291  	return err
   292  }
   293  
   294  // Get returns one entry from the cache
   295  func (c *Cache) Get(ctx context.Context, storageID, spaceID, shareID string, skipSync bool) (*collaboration.Share, error) {
   296  	ctx, span := tracer.Start(ctx, "Get")
   297  	defer span.End()
   298  	span.SetAttributes(attribute.String("cs3.storageid", storageID), attribute.String("cs3.spaceid", spaceID), attribute.String("cs3.shareid", shareID))
   299  
   300  	unlock := c.LockSpace(spaceID)
   301  	defer unlock()
   302  	span.AddEvent("got lock")
   303  
   304  	if !skipSync {
   305  		// sync cache, maybe our data is outdated
   306  		err := c.syncWithLock(ctx, storageID, spaceID)
   307  		if err != nil {
   308  			return nil, err
   309  		}
   310  	}
   311  
   312  	spaces, ok := c.Providers.Load(storageID)
   313  	if !ok {
   314  		return nil, nil
   315  	}
   316  	space, ok := spaces.Spaces.Load(spaceID)
   317  	if !ok {
   318  		return nil, nil
   319  	}
   320  	return space.Shares[shareID], nil
   321  }
   322  
   323  // All returns all entries in the storage
   324  func (c *Cache) All(ctx context.Context) (*mtimesyncedcache.Map[string, *Spaces], error) {
   325  	ctx, span := tracer.Start(ctx, "All")
   326  	defer span.End()
   327  
   328  	providers, err := c.storage.ListDir(ctx, "/storages")
   329  	if err != nil {
   330  		return nil, err
   331  	}
   332  	for _, provider := range providers {
   333  		storageID := provider.Name
   334  		spaces, err := c.storage.ListDir(ctx, path.Join("/storages", storageID))
   335  		if err != nil {
   336  			return nil, err
   337  		}
   338  		for _, space := range spaces {
   339  			spaceID := strings.TrimSuffix(space.Name, ".json")
   340  
   341  			unlock := c.LockSpace(spaceID)
   342  			span.AddEvent("got lock for space " + spaceID)
   343  			if err := c.syncWithLock(ctx, storageID, spaceID); err != nil {
   344  				return nil, err
   345  			}
   346  			unlock()
   347  		}
   348  	}
   349  
   350  	return &c.Providers, nil
   351  }
   352  
   353  // ListSpace returns the list of shares in a given space
   354  func (c *Cache) ListSpace(ctx context.Context, storageID, spaceID string) (*Shares, error) {
   355  	ctx, span := tracer.Start(ctx, "ListSpace")
   356  	defer span.End()
   357  	span.SetAttributes(attribute.String("cs3.storageid", storageID), attribute.String("cs3.spaceid", spaceID))
   358  
   359  	unlock := c.LockSpace(spaceID)
   360  	defer unlock()
   361  	span.AddEvent("got lock")
   362  
   363  	// sync cache, maybe our data is outdated
   364  	err := c.syncWithLock(ctx, storageID, spaceID)
   365  	if err != nil {
   366  		return nil, err
   367  	}
   368  
   369  	spaces, ok := c.Providers.Load(storageID)
   370  	if !ok {
   371  		return &Shares{}, nil
   372  	}
   373  
   374  	space, ok := spaces.Spaces.Load(spaceID)
   375  	if !ok {
   376  		return &Shares{}, nil
   377  	}
   378  
   379  	shares := &Shares{
   380  		Shares: maps.Clone(space.Shares),
   381  		Etag:   space.Etag,
   382  	}
   383  	return shares, nil
   384  }
   385  
   386  // Persist persists the data of one space
   387  func (c *Cache) Persist(ctx context.Context, storageID, spaceID string) error {
   388  	ctx, span := tracer.Start(ctx, "Persist")
   389  	defer span.End()
   390  	span.SetAttributes(attribute.String("cs3.storageid", storageID), attribute.String("cs3.spaceid", spaceID))
   391  
   392  	spaces, ok := c.Providers.Load(storageID)
   393  	if !ok {
   394  		span.AddEvent("nothing to persist")
   395  		span.SetStatus(codes.Ok, "")
   396  		return nil
   397  	}
   398  	space, ok := spaces.Spaces.Load(spaceID)
   399  	if !ok {
   400  		span.AddEvent("nothing to persist")
   401  		span.SetStatus(codes.Ok, "")
   402  		return nil
   403  	}
   404  	span.SetAttributes(attribute.String("BeforeEtag", space.Etag))
   405  	log := appctx.GetLogger(ctx).With().Str("storageID", storageID).Str("spaceID", spaceID).Logger()
   406  	log = log.With().Str("BeforeEtag", space.Etag).Logger()
   407  
   408  	createdBytes, err := json.Marshal(space)
   409  	if err != nil {
   410  		span.RecordError(err)
   411  		span.SetStatus(codes.Error, err.Error())
   412  		return err
   413  	}
   414  	jsonPath := spaceJSONPath(storageID, spaceID)
   415  	if err := c.storage.MakeDirIfNotExist(ctx, path.Dir(jsonPath)); err != nil {
   416  		span.RecordError(err)
   417  		span.SetStatus(codes.Error, err.Error())
   418  		return err
   419  	}
   420  
   421  	span.SetAttributes(attribute.String("etag", space.Etag))
   422  
   423  	ur := metadata.UploadRequest{
   424  		Path:        jsonPath,
   425  		Content:     createdBytes,
   426  		IfMatchEtag: space.Etag,
   427  	}
   428  	// 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
   429  	// > If the field value is "*", the condition is false if the origin server has a current representation for the target resource.
   430  	if space.Etag == "" {
   431  		ur.IfNoneMatch = []string{"*"}
   432  	}
   433  
   434  	res, err := c.storage.Upload(ctx, ur)
   435  	if err != nil {
   436  		span.RecordError(err)
   437  		span.SetStatus(codes.Error, err.Error())
   438  		log.Debug().Err(err).Msg("persisting provider cache failed")
   439  		return err
   440  	}
   441  	space.Etag = res.Etag
   442  
   443  	span.SetStatus(codes.Ok, "")
   444  	shares := []string{}
   445  	for _, s := range space.Shares {
   446  		shares = append(shares, s.GetId().GetOpaqueId())
   447  	}
   448  	log.Debug().Str("AfterEtag", space.Etag).Interface("Shares", shares).Msg("persisted provider cache")
   449  	return nil
   450  }
   451  
   452  // PurgeSpace removes a space from the cache
   453  func (c *Cache) PurgeSpace(ctx context.Context, storageID, spaceID string) error {
   454  	ctx, span := tracer.Start(ctx, "PurgeSpace")
   455  	defer span.End()
   456  
   457  	unlock := c.LockSpace(spaceID)
   458  	defer unlock()
   459  	span.AddEvent("got lock")
   460  
   461  	if !c.isSpaceCached(storageID, spaceID) {
   462  		err := c.syncWithLock(ctx, storageID, spaceID)
   463  		if err != nil {
   464  			return err
   465  		}
   466  	}
   467  
   468  	spaces, ok := c.Providers.Load(storageID)
   469  	if !ok {
   470  		return nil
   471  	}
   472  	newShares := &Shares{}
   473  	if space, ok := spaces.Spaces.Load(spaceID); ok {
   474  		newShares.Etag = space.Etag // keep the etag to allow overwriting the state on the server
   475  	}
   476  	spaces.Spaces.Store(spaceID, newShares)
   477  
   478  	return c.Persist(ctx, storageID, spaceID)
   479  }
   480  
   481  func (c *Cache) syncWithLock(ctx context.Context, storageID, spaceID string) error {
   482  	ctx, span := tracer.Start(ctx, "syncWithLock")
   483  	defer span.End()
   484  
   485  	c.initializeIfNeeded(storageID, spaceID)
   486  
   487  	spaces, _ := c.Providers.Load(storageID)
   488  	space, _ := spaces.Spaces.Load(spaceID)
   489  	span.SetAttributes(attribute.String("cs3.storageid", storageID), attribute.String("cs3.spaceid", spaceID), attribute.String("etag", space.Etag))
   490  	log := appctx.GetLogger(ctx).With().Str("storageID", storageID).Str("spaceID", spaceID).Str("etag", space.Etag).Str("hostname", os.Getenv("HOSTNAME")).Logger()
   491  
   492  	dlreq := metadata.DownloadRequest{
   493  		Path: spaceJSONPath(storageID, spaceID),
   494  	}
   495  	// when we know an etag, only download if it changed remotely
   496  	if space.Etag != "" {
   497  		dlreq.IfNoneMatch = []string{space.Etag}
   498  	}
   499  
   500  	dlres, err := c.storage.Download(ctx, dlreq)
   501  	switch err.(type) {
   502  	case nil:
   503  		span.AddEvent("updating local cache")
   504  	case errtypes.NotFound:
   505  		span.SetStatus(codes.Ok, "")
   506  		return nil
   507  	case errtypes.NotModified:
   508  		span.SetStatus(codes.Ok, "")
   509  		return nil
   510  	default:
   511  		span.RecordError(err)
   512  		span.SetStatus(codes.Error, "downloading provider cache failed")
   513  		return err
   514  	}
   515  
   516  	span.AddEvent("updating local cache")
   517  	newShares := &Shares{}
   518  	err = json.Unmarshal(dlres.Content, newShares)
   519  	if err != nil {
   520  		span.RecordError(err)
   521  		span.SetStatus(codes.Error, "unmarshaling provider cache failed")
   522  		log.Error().Err(err).Msg("unmarshaling provider cache failed")
   523  		return err
   524  	}
   525  	newShares.Etag = dlres.Etag
   526  
   527  	spaces.Spaces.Store(spaceID, newShares)
   528  	span.SetStatus(codes.Ok, "")
   529  	return nil
   530  }
   531  
   532  func (c *Cache) initializeIfNeeded(storageID, spaceID string) {
   533  	spaces, _ := c.Providers.LoadOrStore(storageID, &Spaces{
   534  		Spaces: mtimesyncedcache.Map[string, *Shares]{},
   535  	})
   536  	_, _ = spaces.Spaces.LoadOrStore(spaceID, &Shares{
   537  		Shares: map[string]*collaboration.Share{},
   538  	})
   539  }
   540  
   541  func spaceJSONPath(storageID, spaceID string) string {
   542  	return filepath.Join("/storages", storageID, spaceID+".json")
   543  }