This is an archive of the discontinued Mercurial Phabricator instance.

localrepo: iteratively derive local repository type
ClosedPublic

Authored by indygreg on Sep 18 2018, 6:40 PM.

Details

Summary

This commit implements the dynamic local repository type derivation
that was explained in the recent commit
bfeab472e3c0 "localrepo: create new function for instantiating a local
repo object."

Instead of a static localrepository class/type which must be customized
after construction, we now dynamically construct a type by building up
base classes/types to represent specific repository interfaces.

Conceptually, the end state is similar to what was happening when
various extensions would monkeypatch the class of newly-constructed
repo instances. However, the approach is inverted. Instead of making
the instance then customizing it, we do the customization up front
by influencing the behavior of the type then we instantiate that
custom type.

This approach gives us much more flexibility. For example, we can
use completely separate classes for implementing different aspects
of the repository. For example, we could have one class representing
revlog-based file storage and another representing non-revlog based
file storage. When then choose which implementation to use based on
the presence of repo requirements.

A concern with this approach is that it creates a lot more types
and complexity and that complexity adds overhead. Yes, it is true that
this approach will result in more types being created. Yes, this is
more complicated than traditional "instantiate a static type." However,
I believe the alternatives to supporting alternate storage backends
are just as complicated. (Before I arrived at this solution, I had
patches storing factory functions on local repo instances for e.g.
constructing a file storage instance. We ended up having a handful
of these. And this was logically identical to assigning custom
methods. Since we were logically changing the type of the instance,
I figured it would be better to just use specialized types instead
of introducing levels of abstraction at run-time.)

On the performance front, I don't believe that having N base classes
has any significant performance overhead compared to just a single base
class. Intuition says that Python will need to iterate the base classes
to find an attribute. However, CPython caches method lookups: as long as
the class or MRO isn't changing, method attribute lookup should be
constant time after first access. And non-method attributes are stored
in dict, of which there is only 1 per object, so the number of
base classes for dict is irrelevant.

Anyway, this commit splits up the monolithic completelocalrepository
interface into sub-interfaces: 1 for file storage and 1 representing
everything else.

We've taught `makelocalrepository()` to call a series of factory
functions which will produce types implementing specific interfaces.
It then calls type() to create a new type from the built-up list of
base types.

This commit should be considered a start and not the end state. I
suspect we'll hit a number of problems as we start to implement
alternate storage backends:

  • Passing custom arguments to init and setting custom attributes on dict.
  • Customizing the set of interfaces that are needed. e.g. the "readonly" intent could translate to not requesting an interface providing methods related to writing.
  • More ergonomic way for extensions to insert themselves so their callbacks aren't unconditionally called.
  • Wanting to modify vfs instances, other arguments passed to init.

That being said, this code is usable in its current state and I'm
convinced future commits will demonstrate the value in this approach.

Diff Detail

Repository
rHG Mercurial
Lint
Automatic diff as part of commit; lint not applicable.
Unit
Automatic diff as part of commit; unit tests not applicable.

Event Timeline

indygreg created this revision.Sep 18 2018, 6:40 PM
yuja added a subscriber: yuja.Sep 23 2018, 5:30 AM

+ Extensions should wrap these factory functions to customize repository type
+ creation. Note that an extension's wrapped function may be called even if
+ that extension is not loaded for the repo being constructed. Extensions
+ should check if their `__name__` appears in the
+ `extensionmodulenames` set passed to the factory function and no-op if
+ not.

I assume this will be revisited later. I think it's a source of bugs to
relying on extensions to check if they are enabled. It's also cumbersome
to wrap a function referenced from another table.

This revision was automatically updated to reflect the committed changes.
In D4642#71269, @yuja wrote:

+ Extensions should wrap these factory functions to customize repository type
+ creation. Note that an extension's wrapped function may be called even if
+ that extension is not loaded for the repo being constructed. Extensions
+ should check if their `__name__` appears in the
+ `extensionmodulenames` set passed to the factory function and no-op if
+ not.

I assume this will be revisited later. I think it's a source of bugs to
relying on extensions to check if they are enabled. It's also cumbersome
to wrap a function referenced from another table.

I agree it is not great and I hope to revisit this problem.

FWIW we have similar problems with extensions.wrapfunction(). Many function wrappings bury their head in the sand with regards to universal function wrapping in multi-repo contexts. e.g. in hgweb, repo A can load an extension which wraps a function. Repo B doesn't want that extension loaded but the process still has the function wrapped. I'm not sure how to best handle this :/

In D4642#71269, @yuja wrote:

+ Extensions should wrap these factory functions to customize repository type
+ creation. Note that an extension's wrapped function may be called even if
+ that extension is not loaded for the repo being constructed. Extensions
+ should check if their `__name__` appears in the
+ `extensionmodulenames` set passed to the factory function and no-op if
+ not.

I assume this will be revisited later. I think it's a source of bugs to
relying on extensions to check if they are enabled. It's also cumbersome
to wrap a function referenced from another table.

I agree it is not great and I hope to revisit this problem.
FWIW we have similar problems with extensions.wrapfunction(). Many function wrappings bury their head in the sand with regards to universal function wrapping in multi-repo contexts. e.g. in hgweb, repo A can load an extension which wraps a function. Repo B doesn't want that extension loaded but the process still has the function wrapped. I'm not sure how to best handle this :/

I like the @decorator style registration that evolve does. I wonder if that can be combined with the repository "features" functionality, such that the extension can still globally wrap functions, and add features in say, reposetup(). The decorator could be declared with a feature that needs to be present, and that backing code check for it by default before either calling the wrapper or orig(). I guess one problem is that not every function takes a repo. But without a repo, I'm not sure how any decision can be made outside of individual extensions.

Even if this only covers 75% of the cases, it at least gives visibility to the problem to extension authors. I didn't realize it was a problem until I ran into it with LFS.

That's a good idea to use decorators for this problem space! I'll think about that as I wrote more code for instantiating repo types...