github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kbfs/libgit/autogit_manager.go (about)

     1  // Copyright 2017 Keybase Inc. All rights reserved.
     2  // Use of this source code is governed by a BSD
     3  // license that can be found in the LICENSE file.
     4  
     5  package libgit
     6  
     7  import (
     8  	"context"
     9  	"os"
    10  	"sync"
    11  
    12  	lru "github.com/hashicorp/golang-lru"
    13  	"github.com/keybase/client/go/kbfs/data"
    14  	"github.com/keybase/client/go/kbfs/libfs"
    15  	"github.com/keybase/client/go/kbfs/libkbfs"
    16  	"github.com/keybase/client/go/kbfs/tlf"
    17  	"github.com/keybase/client/go/kbfs/tlfhandle"
    18  	"github.com/keybase/client/go/logger"
    19  	"github.com/keybase/client/go/protocol/keybase1"
    20  	"github.com/pkg/errors"
    21  	"gopkg.in/src-d/go-git.v4/plumbing"
    22  )
    23  
    24  const (
    25  	// Debug tag ID for an individual autogit operation
    26  	ctxAutogitOpID = "AGID"
    27  )
    28  
    29  type ctxAutogitTagKey int
    30  
    31  const (
    32  	ctxAutogitIDKey ctxAutogitTagKey = iota
    33  )
    34  
    35  type browserCacheKey struct {
    36  	fs       *libfs.FS
    37  	repoName string
    38  	branch   plumbing.ReferenceName
    39  	subdir   string
    40  }
    41  
    42  type browserCacheValue struct {
    43  	repoFS  *libfs.FS
    44  	browser *Browser
    45  }
    46  
    47  // AutogitManager can clone and pull source git repos into a
    48  // destination folder, potentially across different TLFs.  New
    49  // requests for an operation in a destination repo are blocked by any
    50  // ongoing requests for the same folder, and multiple outstanding
    51  // requests for the same destination folder get rolled up into one.
    52  type AutogitManager struct {
    53  	config   libkbfs.Config
    54  	log      logger.Logger
    55  	deferLog logger.Logger
    56  
    57  	registryLock           sync.RWMutex
    58  	registeredFBs          map[data.FolderBranch]bool
    59  	repoNodesForWatchedIDs map[libkbfs.NodeID]*repoDirNode
    60  	watchedNodes           []libkbfs.Node // preventing GC on the watched nodes
    61  	deleteCancels          map[string]context.CancelFunc
    62  	shutdown               bool
    63  
    64  	sharedInBrowserCache sharedInBrowserCache
    65  
    66  	browserLock  sync.Mutex
    67  	browserCache *lru.Cache
    68  
    69  	doRemoveSelfCheckouts sync.Once
    70  }
    71  
    72  // NewAutogitManager constructs a new AutogitManager instance.
    73  func NewAutogitManager(
    74  	config libkbfs.Config, browserCacheSize int) *AutogitManager {
    75  	log := config.MakeLogger("")
    76  	browserCache, err := lru.New(browserCacheSize)
    77  	if err != nil {
    78  		panic(err.Error())
    79  	}
    80  	sharedCache, err := newLRUSharedInBrowserCache()
    81  	if err != nil {
    82  		panic(err.Error())
    83  	}
    84  
    85  	return &AutogitManager{
    86  		config:                 config,
    87  		log:                    log,
    88  		deferLog:               log.CloneWithAddedDepth(1),
    89  		registeredFBs:          make(map[data.FolderBranch]bool),
    90  		repoNodesForWatchedIDs: make(map[libkbfs.NodeID]*repoDirNode),
    91  		deleteCancels:          make(map[string]context.CancelFunc),
    92  		browserCache:           browserCache,
    93  		sharedInBrowserCache:   sharedCache,
    94  	}
    95  }
    96  
    97  // Shutdown shuts down this manager.
    98  func (am *AutogitManager) Shutdown() {
    99  	am.registryLock.Lock()
   100  	defer am.registryLock.Unlock()
   101  	am.shutdown = true
   102  	for _, cancel := range am.deleteCancels {
   103  		cancel()
   104  	}
   105  }
   106  
   107  func (am *AutogitManager) removeOldCheckoutsForHandle(
   108  	ctx context.Context, h *tlfhandle.Handle, branch data.BranchName) {
   109  	// Make an "unwrapped" FS, so we don't end up recursively entering
   110  	// the virtual autogit nodes again.
   111  	fs, err := libfs.NewUnwrappedFS(
   112  		ctx, am.config, h, branch, "", "", keybase1.MDPriorityNormal)
   113  	if err != nil {
   114  		am.log.CDebugf(ctx, "Error making unwrapped FS for TLF %s: %+v",
   115  			h.GetCanonicalPath(), err)
   116  		return
   117  	}
   118  
   119  	fi, err := fs.Stat(AutogitRoot)
   120  	if os.IsNotExist(errors.Cause(err)) {
   121  		// No autogit repos to remove.
   122  		return
   123  	} else if err != nil {
   124  		am.log.CDebugf(ctx,
   125  			"Error checking autogit in unwrapped FS for TLF %s: %+v",
   126  			h.GetCanonicalPath(), err)
   127  		return
   128  	}
   129  
   130  	ctx, ok := func() (context.Context, bool) {
   131  		am.registryLock.Lock()
   132  		defer am.registryLock.Unlock()
   133  		if am.shutdown {
   134  			return nil, false
   135  		}
   136  		p := h.GetCanonicalPath()
   137  		if _, ok := am.deleteCancels[p]; ok {
   138  			return nil, false
   139  		}
   140  
   141  		ctx, cancel := context.WithCancel(ctx)
   142  		am.deleteCancels[p] = cancel
   143  		return ctx, true
   144  	}()
   145  	if !ok {
   146  		return
   147  	}
   148  
   149  	am.log.CDebugf(ctx, "Recursively deleting old autogit data in TLF %s",
   150  		h.GetCanonicalPath())
   151  	defer func() {
   152  		am.log.CDebugf(ctx, "Recursive delete of autogit done: %+v", err)
   153  		am.registryLock.Lock()
   154  		defer am.registryLock.Unlock()
   155  		delete(am.deleteCancels, h.GetCanonicalPath())
   156  	}()
   157  	err = libfs.RecursiveDelete(ctx, fs, fi)
   158  }
   159  
   160  func (am *AutogitManager) removeOldCheckouts(node libkbfs.Node) {
   161  	ctx := libkbfs.CtxWithRandomIDReplayable(
   162  		context.Background(), ctxAutogitIDKey, ctxAutogitOpID, am.log)
   163  
   164  	h, err := am.config.KBFSOps().GetTLFHandle(ctx, node)
   165  	if err != nil {
   166  		am.log.CDebugf(ctx, "Error getting handle: %+v", err)
   167  		return
   168  	}
   169  
   170  	am.removeOldCheckoutsForHandle(ctx, h, node.GetFolderBranch().Branch)
   171  }
   172  
   173  func (am *AutogitManager) removeSelfCheckouts() {
   174  	ctx := libkbfs.CtxWithRandomIDReplayable(
   175  		context.Background(), ctxAutogitIDKey, ctxAutogitOpID, am.log)
   176  
   177  	session, err := am.config.KBPKI().GetCurrentSession(ctx)
   178  	if err != nil {
   179  		am.log.CDebugf(ctx,
   180  			"Unable to get session; ignoring self-autogit delete: +%v", err)
   181  		return
   182  	}
   183  
   184  	h, err := libkbfs.GetHandleFromFolderNameAndType(
   185  		ctx, am.config.KBPKI(), am.config.MDOps(),
   186  		am.config, string(session.Name), tlf.Private)
   187  	if err != nil {
   188  		am.log.CDebugf(ctx,
   189  			"Unable to get private handle; ignoring self-autogit delete: +%v",
   190  			err)
   191  		return
   192  	}
   193  
   194  	am.removeOldCheckoutsForHandle(ctx, h, data.MasterBranch)
   195  }
   196  
   197  func (am *AutogitManager) registerRepoNode(
   198  	nodeToWatch libkbfs.Node, rdn *repoDirNode) {
   199  	am.registryLock.Lock()
   200  	defer am.registryLock.Unlock()
   201  	am.repoNodesForWatchedIDs[nodeToWatch.GetID()] = rdn
   202  	fb := nodeToWatch.GetFolderBranch()
   203  	if am.registeredFBs[fb] {
   204  		return
   205  	}
   206  
   207  	go am.removeOldCheckouts(rdn)
   208  	am.doRemoveSelfCheckouts.Do(func() { go am.removeSelfCheckouts() })
   209  	am.watchedNodes = append(am.watchedNodes, nodeToWatch)
   210  	err := am.config.Notifier().RegisterForChanges(
   211  		[]data.FolderBranch{fb}, am)
   212  	if err != nil {
   213  		am.log.CWarningf(
   214  			context.TODO(), "Error registering %s: +%v", fb.Tlf, err)
   215  		return
   216  	}
   217  	am.registeredFBs[fb] = true
   218  }
   219  
   220  // LocalChange implements the libkbfs.Observer interface for AutogitManager.
   221  func (am *AutogitManager) LocalChange(
   222  	ctx context.Context, node libkbfs.Node, wr libkbfs.WriteRange) {
   223  	// Do nothing.
   224  }
   225  
   226  func (am *AutogitManager) getNodesToInvalidate(
   227  	affectedNodeIDs []libkbfs.NodeID) (
   228  	nodes []libkbfs.Node, repoNodeIDs []libkbfs.NodeID) {
   229  	am.registryLock.RLock()
   230  	defer am.registryLock.RUnlock()
   231  	for _, nodeID := range affectedNodeIDs {
   232  		node, ok := am.repoNodesForWatchedIDs[nodeID]
   233  		if ok {
   234  			nodes = append(nodes, node)
   235  			repoNodeIDs = append(repoNodeIDs, nodeID)
   236  		}
   237  	}
   238  	return nodes, repoNodeIDs
   239  }
   240  
   241  func (am *AutogitManager) clearInvalidatedBrowsers(
   242  	repoNodeIDs []libkbfs.NodeID) {
   243  	am.browserLock.Lock()
   244  	defer am.browserLock.Unlock()
   245  
   246  	keys := am.browserCache.Keys()
   247  	for _, k := range keys {
   248  		// Clear all cached browsers associated with
   249  		tmp, ok := am.browserCache.Get(k)
   250  		if !ok {
   251  			continue
   252  		}
   253  		v, ok := tmp.(browserCacheValue)
   254  		if !ok {
   255  			continue
   256  		}
   257  		rootNodeID := v.repoFS.RootNode().GetID()
   258  		// Note that in almost all cases, `repoNodeIDs` should only
   259  		// have one entry (since only one repo is updated in a single
   260  		// metadata update), so iterating here should be cheaper than
   261  		// making a map.
   262  		for _, nodeID := range repoNodeIDs {
   263  			if rootNodeID == nodeID {
   264  				am.log.CDebugf(
   265  					context.TODO(), "Invalidating browser for %s",
   266  					v.repoFS.Root())
   267  				am.browserCache.Remove(k)
   268  				break
   269  			}
   270  		}
   271  	}
   272  }
   273  
   274  // BatchChanges implements the libkbfs.Observer interface for AutogitManager.
   275  func (am *AutogitManager) BatchChanges(
   276  	ctx context.Context, _ []libkbfs.NodeChange,
   277  	affectedNodeIDs []libkbfs.NodeID) {
   278  	nodes, repoNodeIDs := am.getNodesToInvalidate(affectedNodeIDs)
   279  	go am.clearInvalidatedBrowsers(repoNodeIDs)
   280  	for _, node := range nodes {
   281  		node := node
   282  		go func() {
   283  			ctx := libkbfs.CtxWithRandomIDReplayable(
   284  				context.Background(), ctxAutogitIDKey, ctxAutogitOpID, am.log)
   285  			err := am.config.KBFSOps().InvalidateNodeAndChildren(ctx, node)
   286  			if err != nil {
   287  				am.log.CDebugf(ctx, "Error invalidating children: %+v", err)
   288  			}
   289  		}()
   290  	}
   291  }
   292  
   293  // TlfHandleChange implements the libkbfs.Observer interface for
   294  // AutogitManager.
   295  func (am *AutogitManager) TlfHandleChange(
   296  	ctx context.Context, newHandle *tlfhandle.Handle) {
   297  	// Do nothing.
   298  }
   299  
   300  func (am *AutogitManager) getBrowserForRepoLocked(
   301  	ctx context.Context, gitFS *libfs.FS, repoName string,
   302  	branch plumbing.ReferenceName, subdir string) (*libfs.FS, *Browser, error) {
   303  	repoName = normalizeRepoName(repoName)
   304  	key := browserCacheKey{gitFS, repoName, branch, subdir}
   305  	tmp, ok := am.browserCache.Get(key)
   306  	if ok {
   307  		b, ok := tmp.(browserCacheValue)
   308  		if !ok {
   309  			return nil, nil, errors.Errorf("Bad browser in cache: %T", tmp)
   310  		}
   311  		return b.repoFS, b.browser, nil
   312  	}
   313  
   314  	// It's kind of dumb to hold the browser lock through all of this,
   315  	// but it doesn't seem worthwhile to build the whole
   316  	// channel/notification system that would be needed to manage
   317  	// multiple concurrent requests for the same repo node.
   318  
   319  	am.log.CDebugf(ctx, "Making browser for repo=%s, branch=%s, subdir=%s",
   320  		repoName, branch, subdir)
   321  
   322  	// Recurse to get the root browser, and then chroot to the subdir.
   323  	if subdir != "" {
   324  		repoFS, rootB, err := am.getBrowserForRepoLocked(
   325  			ctx, gitFS, repoName, branch, "")
   326  		if err != nil {
   327  			return nil, nil, err
   328  		}
   329  
   330  		b, err := rootB.Chroot(subdir)
   331  		if err != nil {
   332  			return nil, nil, err
   333  		}
   334  		browser, ok := b.(*Browser)
   335  		if !ok {
   336  			return nil, nil, errors.Errorf("Bad browser type: %T", b)
   337  		}
   338  		am.browserCache.Add(key, browserCacheValue{repoFS, browser})
   339  		return repoFS, browser, nil
   340  	}
   341  
   342  	billyFS, err := gitFS.Chroot(repoName)
   343  	if err != nil {
   344  		return nil, nil, err
   345  	}
   346  	repoFS := billyFS.(*libfs.FS)
   347  	browser, err := NewBrowser(
   348  		repoFS, am.config.Clock(), branch, am.sharedInBrowserCache)
   349  	if err != nil {
   350  		return nil, nil, err
   351  	}
   352  	am.browserCache.Add(key, browserCacheValue{repoFS, browser})
   353  	return repoFS, browser, nil
   354  }
   355  
   356  // GetBrowserForRepo returns the root FS for the specified repo and a
   357  // `Browser` for the branch and subdir.
   358  func (am *AutogitManager) GetBrowserForRepo(
   359  	ctx context.Context, gitFS *libfs.FS, repoName string,
   360  	branch plumbing.ReferenceName, subdir string) (*libfs.FS, *Browser, error) {
   361  	am.browserLock.Lock()
   362  	defer am.browserLock.Unlock()
   363  	return am.getBrowserForRepoLocked(ctx, gitFS, repoName, branch, subdir)
   364  }
   365  
   366  // StartAutogit launches autogit, and returns a function that should
   367  // be called on shutdown.
   368  func StartAutogit(config libkbfs.Config, browserCacheSize int) func() {
   369  	am := NewAutogitManager(config, browserCacheSize)
   370  	rw := rootWrapper{am}
   371  	config.AddRootNodeWrapper(rw.wrap)
   372  	return am.Shutdown
   373  }