Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add fx options plugin #9010

Merged
merged 1 commit into from
Aug 12, 2022
Merged

feat: add fx options plugin #9010

merged 1 commit into from
Aug 12, 2022

Conversation

guseggert
Copy link
Contributor

@guseggert guseggert commented Jun 3, 2022

This adds a plugin interface that lets the plugin modify the fx
options that are passed to fx when the app is initialized. This means
plugins can inject their own implementations of IPFS interfaces. This
enables granular customization of go-ipfs behavior by plugins, such
as:

  • Bitswap with custom filters (e.g. for CID blocking)

  • Custom interface implementations such as Pinner or DAGService

  • Dynamic configuration of libp2p ...

One downside of this is that we're exposing the entire dependency
graph, init hooks, initialization, etc. to users, so this comes with a
caveat that we reserve the right to make breaking changes to the graph
structure and initialization logic (although this historically happens
rarely). If these things are changed, we should mention them in
release notes and changelogs though, since they could impact users of
this plugin interface.

I'm not particularly fond of DI frameworks (and neither are some of
the folks work on/near go-ipfs), but it seems unlikely that somebody
will rewrite the dependency wiring and lifecycle hooks of go-ipfs, and
add dynamic extension points, so this seems like a palatable
compromise.

There are also problems that we should clean up in how model the
go-ipfs app in fx, such as:

  • We make extensive use of nested fx.Options, which fx itself
    discourages because it "limits the user's ability to customize their
    application". It should be easy to flatten these out into a single
    []fx.Option slice.

  • We pass around a list of opaque libp2p opts, which makes it hard to
    customize after-the-fact...we should consider naming each of these
    opts and providing them to fx as proper dependencies, so that they can
    be explicitly overridden.

  • We call fx.Invoke() in some places with anonymous functions. We
    should instead only pass exported functions to fx.Invoke(), so that
    they have exported names, which would make it easier to remove/augment
    the invocations that happen when the app is initialized.

These aren't blocking issues, they just make it harder and more
brittle to customize go-ipfs with this plugin.

Closes #7653

@guseggert
Copy link
Contributor Author

To elaborate on the plugin interface: the reason it takes the full list of fx options and returns the full list, is to maximize the ability to customize what gets passed to fx. Currently this isn't super useful because we pack everything into opaque fx.Options groups which prevents customization, but as mentioned in the commit msg, if we stop using those and flatten everything out into a single slice of fx.Option, it gets way easier to customize, because fx.Option has a solid string representation. For example, here's the output of the option strings for the current opts:

fx.Options(fx.Options(fx.Provide(github.com/ipfs/go-ipfs/core/node.(*BuildCfg).options.func1()), fx.Provide(github.com/ipfs/go-ipfs/core/node.(*BuildCfg).options.func3()), fx.Provide(github.com/ipfs/go-ipfs/core/node.(*BuildCfg).options.func4()), fx.Provide(github.com/ipfs/go-ipfs/core/node.(*BuildCfg).options.func2())), fx.Provide(github.com/ipfs/go-ipfs/core/node.baseProcess()), fx.Options(fx.Provide(github.com/ipfs/go-ipfs/core/node.RepoConfig()), fx.Provide(github.com/ipfs/go-ipfs/core/node.Datastore()), fx.Provide(github.com/ipfs/go-ipfs/core/node.BaseBlockstoreCtor.func1()), fx.Provide(github.com/ipfs/go-ipfs/core/node.GcBlockstoreCtor())), fx.Options(fx.Provide(github.com/ipfs/go-ipfs/core/node.Identity.func2()), fx.Provide(github.com/ipfs/go-ipfs/core/node.PrivateKey.func1()), fx.Provide(github.com/ipfs/go-ipfs/core/node/libp2p.Peerstore()), fx.Invoke(github.com/ipfs/go-ipfs/core/node/libp2p.PstoreAddSelfKeys())), fx.Options(fx.Provide(github.com/ipfs/go-ipfs/core/node.RecordValidator())), fx.Options(fx.Provide(github.com/ipfs/go-ipfs/core/node.OnlineExchange.func1()), fx.Options(), fx.Provide(github.com/ipfs/go-ipfs/core/node.DNSResolver()), fx.Provide(github.com/ipfs/go-ipfs/core/node.Namesys.func1()), fx.Provide(github.com/ipfs/go-ipfs/core/node.Peering()), fx.Invoke(github.com/ipfs/go-ipfs/core/node.PeerWith.func1()), fx.Invoke(github.com/ipfs/go-ipfs/core/node.IpnsRepublisher.func1()), fx.Provide(github.com/ipfs/go-ipfs/p2p.New()), fx.Options(fx.Options(fx.Provide(github.com/ipfs/go-ipfs/core/node/libp2p.init.func2()), fx.Provide(github.com/ipfs/go-ipfs/core/node/libp2p.PNet()), fx.Provide(github.com/ipfs/go-ipfs/core/node/libp2p.ConnectionManager()), fx.Provide(github.com/ipfs/go-ipfs/core/node/libp2p.Host()), fx.Provide(github.com/ipfs/go-ipfs/core/node/libp2p.MultiaddrResolver()), fx.Provide(github.com/ipfs/go-ipfs/core/node/libp2p.DiscoveryHandler()), fx.Invoke(github.com/ipfs/go-ipfs/core/node/libp2p.PNetChecker())), fx.Supply(libp2p.AddrInfoChan), fx.Provide(github.com/ipfs/go-ipfs/core/node/libp2p.ResourceManager.func1()), fx.Provide(github.com/ipfs/go-ipfs/core/node/libp2p.AddrFilters.func1()), fx.Provide(github.com/ipfs/go-ipfs/core/node/libp2p.AddrsFactory.func1()), fx.Provide(github.com/ipfs/go-ipfs/core/node/libp2p.SmuxTransport.func1()), fx.Provide(github.com/ipfs/go-ipfs/core/node/libp2p.RelayTransport.func1()), fx.Provide(github.com/ipfs/go-ipfs/core/node/libp2p.RelayService.func1()), fx.Provide(github.com/ipfs/go-ipfs/core/node/libp2p.Transports.func1()), fx.Invoke(github.com/ipfs/go-ipfs/core/node/libp2p.StartListening.func1()), fx.Invoke(github.com/ipfs/go-ipfs/core/node/libp2p.SetupDiscovery.func1()), fx.Provide(github.com/ipfs/go-ipfs/core/node/libp2p.ForceReachability.func1()), fx.Provide(github.com/ipfs/go-ipfs/core/node/libp2p.HolePunching.func1()), fx.Provide(github.com/ipfs/go-ipfs/core/node/libp2p.Security.func2()), fx.Provide(github.com/ipfs/go-ipfs/core/node/libp2p.Routing()), fx.Provide(github.com/ipfs/go-ipfs/core/node/libp2p.BaseRouting.func1()), fx.Options(), fx.Provide(github.com/ipfs/go-ipfs/core/node/libp2p.BandwidthCounter()), fx.Provide(github.com/ipfs/go-ipfs/core/node/libp2p.init.func3()), fx.Provide(github.com/ipfs/go-ipfs/core/node/libp2p.AutoRelay.func1()), fx.Invoke(github.com/ipfs/go-ipfs/core/node/libp2p.AutoRelayFeeder.func1()), fx.Provide(github.com/ipfs/go-ipfs/core/node/libp2p.AutoNATService.func1()), fx.Provide(github.com/ipfs/go-ipfs/core/node/libp2p.ConnectionManager.func1()), fx.Options(), fx.Options()), fx.Options(fx.Options(fx.Provide(github.com/ipfs/go-ipfs/core/node.ProviderQueue()), fx.Provide(github.com/ipfs/go-ipfs/core/node.SimpleProvider()), fx.Provide(github.com/ipfs/go-ipfs-provider/simple.NewBlockstoreProvider()), fx.Provide(github.com/ipfs/go-ipfs/core/node.SimpleReprovider.func1())), fx.Options(), fx.Provide(github.com/ipfs/go-ipfs/core/node.BatchedProviderSys.func1()))), fx.Options(fx.Provide(github.com/ipfs/go-ipfs/core/node.BlockService()), fx.Provide(github.com/ipfs/go-ipfs/core/node.Dag()), fx.Provide(github.com/ipfs/go-ipfs/core/node.FetcherConfig()), fx.Provide(github.com/ipfs/go-ipfs/core/node.Pinning()), fx.Provide(github.com/ipfs/go-ipfs/core/node.Files()))) fx.WithLogger(go.uber.org/fx.glob..func1())

If this were flattened out instead of using fx.Options to group things, then we could easily e.g. remove certain fx.Invoke() entries. This gets even better if we only use named functions for fx.Invoke() instead of anonymous functions, so they look like fx.Invoke(github.com/ipfs/go-ipfs/core/SomeExportedFunc) instead of fx.Invoke(github.com/ipfs/go-ipfs/core/func1(), and they can be easily wrapped at that point. AFAIU, passing fx.Options is functionally equivalent to passing []fx.Option.

In most cases, where you want to customize the implementation of an interface like exchange.Interface (for customizing Bitswap), you just append an fx.Replace() or fx.Decorate() to the opts. Plugin authors should be prefer this, as it is much less likely to break over time.

@guseggert
Copy link
Contributor Author

Here's an example plugin that overrides the default Pinner with a custom one:

func (p *PinnerPlugin) Options(opts []fx.Option) ([]fx.Option, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	cfg, err := config.LoadDefaultConfig(context.Background())
	if err != nil {
		return nil, fmt.Errorf("loading AWS config: %w", err)
	}

	logger, err := zap.NewProduction()
	if err != nil {
		return nil, fmt.Errorf("constructing logger: %w", err)
	}

	smClient := secretsmanager.NewFromConfig(cfg)

	pinsDB, err := db.NewPinsDB(ctx, logger.Sugar(), smClient)
	if err != nil {
		return nil, fmt.Errorf("constructing PinsDB: %w", err)
	}

	pinner := &pinner.Pinner{PinsDB: pinsDB}

	return append(opts, fx.Replace(fx.Annotate(pinner, fx.As(new(pin.Pinner))))), nil
}

@BigLep
Copy link
Contributor

BigLep commented Jun 30, 2022

2022-06-30 conversation:

  • Have a way to exercise the code like install a plugin that does FX.invoke that prints something out and then check in sharness that the debug statement prints out.
  • We're not using go-test because we need Make involved.
  • See if we can avoid Make by using the preload function.
  • After having a way to test/verify, lets switch to "review" and "out of draft"

@BigLep
Copy link
Contributor

BigLep commented Jul 27, 2022

@guseggert : I'm moving to the next iteration. Let me know if you disagree.

@guseggert guseggert force-pushed the feat/fx-plugin branch 3 times, most recently from 3dea41b to cbef0cd Compare August 6, 2022 12:20
@guseggert guseggert marked this pull request as ready for review August 6, 2022 12:30
Copy link
Contributor

@aschmahmann aschmahmann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems reasonable to me, left a couple comments.

We may have to evolve this or the documentation as people play around with it and see how it works, but at least there's a hook now.

core/builder.go Outdated Show resolved Hide resolved
plugin/loader/preload.go Show resolved Hide resolved
This adds a plugin interface that lets the plugin modify the fx
options that are passed to fx when the app is initialized. This means
plugins can inject their own implementations of IPFS interfaces. This
enables granular customization of go-ipfs behavior by plugins, such
as:

- Bitswap with custom filters (e.g. for CID blocking) Custom interface

- implementations such as Pinner or DAGService

- Dynamic configuration of libp2p ...

One downside of this is that we're exposing the entire dependency
graph, init hooks, initialization, etc. to users, so this comes with a
caveat that we reserve the right to make breaking changes to the graph
structure and initialization logic (although this historically happens
rarely). If these things are changed, we should mention them in
release notes and changelogs though, since they could impact users of
this plugin interface.

I'm not particularly fond of DI frameworks (and neither are some of
the folks work on/near go-ipfs), but it seems unlikely that somebody
will rewrite the dependency wiring and lifecycle hooks of go-ipfs, and
add dynamic extension points, so this seems like a palatable
compromise.

There are also problems that we should clean up in how model the
go-ipfs app in fx, such as:

- We make extensive use of nested fx.Options, which fx itself
discourages because it "limits the user's ability to customize their
application". It should be easy to flatten these out into a single
[]fx.Option slice.

- We pass around a list of opaque libp2p opts, which makes it hard to
customize after-the-fact...we should consider naming each of these
opts and providing them to fx as proper dependencies, so that they can
be explicitly overridden.

- We call fx.Invoke() in some places with anonymous functions. We
should instead only pass exported functions to fx.Invoke(), so that
they have exported names, which would make it easier to remove/augment
the invocations that happen when the app is initialized.

These aren't blocking issues, they just make it harder and more
brittle to customize go-ipfs with this plugin.
@BigLep BigLep mentioned this pull request Aug 12, 2022
72 tasks
@@ -113,7 +113,7 @@ require (
go.uber.org/zap v1.21.0
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20220517195934-5e4e11fc645e
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this bump doesn't seem necessary, but also seems highly unlikely to be a problem 😄

Copy link
Contributor

@aschmahmann aschmahmann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, let's get started here and we'll see how people start using it and evolve as needed.

Thanks for pushing this over the finish line 🙏

@aschmahmann aschmahmann merged commit 310dca5 into master Aug 12, 2022
@aschmahmann aschmahmann deleted the feat/fx-plugin branch August 12, 2022 21:04
@aschmahmann
Copy link
Contributor

@guseggert I realized after merging that while this is fine we should add some docs about this to https://github.com/ipfs/kubo/blob/master/docs/plugins.md#plugin-types. Do you mind taking care of that in a follow up PR?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
No open projects
Archived in project
Development

Successfully merging this pull request may close these issues.

IPFS Option plugins
3 participants