Implement local lock cache, support querying it#1760
Conversation
API version currently has some fields which may go away, or which may be solely internal in future. For now only expose the fields which are always useful.
946f1d0 to
6c80e58
Compare
|
I think with the merging of this PR, |
ttaylorr
left a comment
There was a problem hiding this comment.
Looking good, I left some thoughts about a potential further abstraction you could make along with some nit-picks.
| // path must be relative to the root of the repository | ||
| // Returns the lock id if successful, or an error | ||
| func (c *Client) LockFile(path string) (id string, e error) { | ||
| func (c *Client) LockFile(path string) (Lock, error) { |
There was a problem hiding this comment.
I think returning a *Lock would be appropriate here, since this method doesn't guarantee that locking a file will succeed all of the time, and the zero-value Lock{} doesn't make sense.
There was a problem hiding this comment.
Actually I remember this was deliberately by value now. Returning a non-pointer is important because I don't want to allow the client to modify the original lock, because it's cached and therefore they could corrupt the cache by doing that. This outweighs the zero value Lock concern IMO. Yes I could return a pointer and copy it but it's unnecessary complexity. A zero value in the event of an error is idiomatic anyway (nil is just a zero value pointer after all).
There was a problem hiding this comment.
I wouldn't mind implementing a (l *Lock) Copy() *Lock function, but I don't feel strongly.
There was a problem hiding this comment.
That depends on callers doing the right thing (not modifying return values from the locking client. I think cache corruption is a good enough reason to return a value.
| // that this lock was created. For most server implementations, this | ||
| // should be set to the instant at which the lock was initially | ||
| // received. | ||
| // LockedAt tells you when this lock was acquired. |
| // If localOnly = true, don't query the server & report only own local locks | ||
| func (c *Client) SearchLocks(filter map[string]string, limit int, localOnly bool) (locks []Lock, err error) { | ||
|
|
||
| if localOnly { |
| locks := make([]Lock, 0, len(cachedlocks)) | ||
| for _, l := range cachedlocks { | ||
| // Manually filter by Path/Id | ||
| if (filterByPath && path != l.Path) || |
There was a problem hiding this comment.
I follow this logic, but I think we can make it clearer:
for _, l := range cachedLocks {
matchesPath := path == l.Path
matchesId := id == l.Id
if (filterByPath && matchesPath) || (filterById && matchesId) {
// ...
}
}This does add a level of indentation to the body of the loop, but I think the extra clarity is worth it.
There was a problem hiding this comment.
I hate to be the optimisation nitpicker here but doing it my way avoids comparison operations in the case of !filterByPath or !filterById whereas your way requires the comparison all the time, so I prefer my way 😄 I think it's clear enough.
There was a problem hiding this comment.
I agree that the original is clear enough.
| // that won't be in a path) | ||
| const idKeyPrefix = "*id*://" | ||
|
|
||
| func (c *Client) encodeIdKey(id string) string { |
There was a problem hiding this comment.
Not sure if (c *Client) is the right receiver for this method. What do you think about wrapping tools/kv in this package with functions that understand *Lock and etc, and putting this method there?
package locking
const (
idPrefix string = "*id*://"
)
type LockCache struct {
kv *kv.Store
}
func (c *LockCache) CacheLock(l Lock) error {
// ...
}
func (c *LockCache) encodeId(id string) string {
if !c.isId(id) {
return idPrefix+id
}
return id
}
func (c *LockCache) isId(key string) bool {
return strings.HasPrefix(key, idPrefix)
}There was a problem hiding this comment.
I like this approach a lot. I think it's a nice, clear line of abstraction that we can draw. The *locking.Client would own a *LockCache underneath and delegate into the cache's public methods from there.
There was a problem hiding this comment.
Yeah that's a good shout.
| config.LocalGitStorageDir = oldStore | ||
| }() | ||
|
|
||
| client, err := NewClient(config.NewFrom(config.Values{})) |
There was a problem hiding this comment.
Here's another place where I think the approach I proposed above would yield a win. This test is testing the behavior of caching a lock, but creates a locking client to do so. Moving that behavior down into a more isolated part of the system would mean that this test can be closer to it's subject.
There was a problem hiding this comment.
Yep, it made it better :)
| func (c *Client) cacheUnlockByPath(filePath string) error { | ||
| ilock := c.cache.Get(filePath) | ||
| if ilock != nil { | ||
| lock := ilock.(*Lock) |
There was a problem hiding this comment.
Not that this is likely behavior, but I think that using the two-tuple variant of this cast would be safer in that it'd prevent an unlikely panic:
func (c *Client) cacheUnlockByPath(filePath string) error {
ilock := c.cache.Get(filePath)
if ilock == nil { // <- needed to prevent `(*Lock)(nil)`
return nil
}
if l, ok := ilock.(*Lock); ok {
// ...
}
return nil
}There was a problem hiding this comment.
Cool, I've just collapsed this into:
if lock, ok := ilock.(*Lock); ok && lock != nil {Since the actual casting of nil is safe anyway so long as we check.
| c.cache.Remove(idkey) | ||
| c.cache.Remove(lock.Path) | ||
| } | ||
| return nil |
There was a problem hiding this comment.
Do you think we should return an error if we couldn't find the lock? I think a sentinel value here would be fine:
package locking
var ErrMissingLock error = errors.New("locking: could not find lock")There was a problem hiding this comment.
I always think it's better to fail silently when requesting to remove a non-existent item from any collection; this is idiomatic with core collections and what I would expect from an API.
| return nil | ||
| func (c *Client) cachedLocks() []Lock { | ||
| var locks []Lock | ||
| c.cache.Visit(func(key string, val interface{}) bool { |
technoweenie
left a comment
There was a problem hiding this comment.
I have some naming concerns, but that's it. If you feel strongly about your names, go ahead and merge. Or we can discuss it in the locking-master -> master PR (our last chance to finalize exported names) :)
| } | ||
|
|
||
| // Cache a successful lock for faster local lookup later | ||
| func (c *LockCache) CacheLock(l Lock) error { |
There was a problem hiding this comment.
I think having functions with a Cache prefix is redundant. Especially in the *Client usage: c.cache.CacheUnlockById(id). What do you think about method names like this:
CacheLock->SetCacheUnlockByPath->DeleteByPathCacheUnlockById->DeleteByIdCachedLocks->GetAllor justAll
There was a problem hiding this comment.
Fair point; to keep it distinct from kv I'll make it Add / RemoveByBlah / Locks.
| locks := make([]Lock, 0, len(cachedlocks)) | ||
| for _, l := range cachedlocks { | ||
| // Manually filter by Path/Id | ||
| if (filterByPath && path != l.Path) || |
There was a problem hiding this comment.
I agree that the original is clear enough.
| } | ||
| // Fetch locked files for the current committer and cache them locally | ||
| // This can be used to sync up locked files when moving machines | ||
| func (c *Client) fetchLocksToCache() error { |
There was a problem hiding this comment.
I think something like refreshLocksCache() would be a better name.
ttaylorr
left a comment
There was a problem hiding this comment.
@sinbad - thanks for addressing my comments 😄. I'm with @technoweenie on this one: everything looks good with some naming changes, unless you feel strongly.
The next incremental rewrite of the locking work; this time we're using our key value store to locally cache locks for the current user for more efficient access. This will be used in the next PR to provide efficient support for marking files read-only on checkout if not locked.
Also adds some minor API improvements such as returning a full
Lockstructure on acquiring a lock rather than just an Id.