Open illicitonion opened 2 years ago
/cc @achew22 @sluongng @linzhp
I saw you are experimenting different approaches to skip flaky tests. Uber did quite a bit too with Go. If I understand correctly, you want to skip some tests in a package, but don't want to skip the whole test target, right? So you would mark the tests that you want to skip with @Flaky
. Then you want Gazelle put those classes in flaky_java_test
and the rest in java_test
. Is that right?
Yeah, that's exactly right @linzhp.
I think (1) is definitely a bit too disruptive.
I think after https://github.com/bazelbuild/bazel-gazelle/commit/622d8889c63227a71d6b393ba5e3a8b8a6761466, option (2) is feasible.
We would want to advise folks to compose BaseLang
into their own struct implementation. BaseLang
should contain a sensible default implementation for LoadsWithConfig(...)
and KindsWithConfig(...)
(actual names pending™️) so that on Gazelle version upgrade, folks would just need to add BaseLang
into their struct for an easy, quick fix.
type MyAwesomeLang {
+ language.BazeLang
}
We can then mark Loads()
and Kinds()
as deprecated, rename them in the subsequent patches to OldLoads()
and OldKinds()
before eventual deletion. This migration would require some engineering hours to drive within Gazelle code base plus release notes documentation, but definitely not that much 🤔
Implementing a new interface definitely would be a more backward compatible approach, but I think that would have resulted in a fragmented end state where end user would have more interfaces to worry about during implementation for no apparent benefits.
Gazelle is still before 1.0.0 so breaking change could be accepted, though we definitely should not abuse that ability. 👀
Thanks for the thoughts!
BaseLang
should contain a sensible default implementation forLoadsWithConfig(...)
andKindsWithConfig(...)
This generally sounds reasonable, except that I'm not sure what that sensible default implementation would look like...
I tried implementing this, but because Go doesn't have overridable virtual functions, just composable structs, I don't think there's a way to have BaseLang.KindsWithConfig
call goLang.Kinds
, unless BaseLang
has a self pointer to a language.Language
it can use to do the virtual dispatch on (and there's no automatic way to make that happen, we'd need to have a fiddly BaseLang.Init(Language)
function we'd require folks call)...
Do you know if a way to have BaseLang
's functions be overridable, when called from inside other functions in BaseLang
?
This migration would require some engineering hours to drive within Gazelle code base plus release notes documentation, but definitely not that much 🤔
I'm happy to put in the hours, as long as we can work out how to make it work!
Gazelle is still before 1.0.0 so breaking change could be accepted, though we definitely should not abuse that ability. 👀
One additional piece of fun here is the bzl gazelle implementation in skylib, which effectively is a circular dependency, but we can work out these details!
I think changing the build graph to skip some flaky tests is quite disruptive and yet not flexible enough. The granularity is only at file/class level. You have to disable all test cases in a file even though some of them are not flaky. In addition, the build cache for the test targets are invalidated whenever you want to disable or enable some of the test classes.
Ideally, skipping flaky tests should be a runtime configuration, not a build time configuration: we build the same test binary, and conditionally run some tests out of it. For example, in Uber's Go monorepo, land-blocking jobs skips all unit tests, and a separate non-blocking job run flaky tests. There is also a post land job that run all tests to detect flaky tests. These jobs share the same test binary fetched from remote cache. They read a configuration file at runtime that contains all known flaky test cases and a flag to decide whether to skip them or not.
We did this by patching rules_go right now, but eventually when https://github.com/bazelbuild/rules_go/issues/2785 is implemented, we can use Bazel --test_filter
to do that. I think you should also implement --test_filter
for Java to skip test cases. With that, you CI system can read some configuration file build a --test_filter
. No changes to source code or build graph is needed to skip or enable flaky tests.
Sorry, I simplified our use case to the example of flaky tests (which I agree doesn't necessarily pull its weight as motivation) - a better example for us than flaky tests is tests that require external network, because that requires encoding specific data in the remote execution protocol when running the tests - let's focus on that one:
We have some Java tests which are annotated as requiring network:
@RequiresNetwork
public class ScaryIntegrationTest {
// ...
}
We need to be able to express to our CI system that when communicating with a remote execution system, caching needs to be disabled, and a platform property needs to be passed to enable routing the actions to network-enabled workers.
We currently express this through a modification in the BUILD file - effectively we have a macro which modifies the exec_properties
attribute of a rule, and may modify its tags
attribute.
a separate non-blocking job run flaky tests. There is also a post land job that run all tests to detect flaky tests. These jobs share the same test binary fetched from remote cache. They read a configuration file at runtime that contains all known flaky test cases and a flag to decide whether to skip them or not.
This sounds like you need to make many separate systems aware of this metadata, which also needs storing somewhere (or needs to be generated externally of bazel), which is what we're trying to avoid. We want the test portion our CI pipeline to be a simple bazel test ...
(or some subset of ...
), the same as a user would run on their machine.
If we don't encode in a BUILD file that a test requires access to the network, we lose the ability to bazel test
our targets any more.
Instead we'd need some separate tool needs to be aware of the RequiresNetwork
annotation. And that tool would either need to run bazel
multiple times with different flags (which is problematic for parallelism), or we would need to bazel build
our targets and then run them outside of bazel
(which is a lot of work, e.g. it would require us to implement the remote execution protocol).
I can't really see a world where we can both bazel test
these tests, and not have this information noted in the BUILD file.
But even without this particular use-case, the core of this feature request is effectively to not treat map_kind
as a special directive. Right now, map_kind
can be used anywhere in the repo, but only at a BUILD file granularity - this feature request is to allow any language implementation to have the power of map_kind
based on its own criteria, rather than requiring effectively a global override. A team being able to configure that, say, half of the targets generated in a BUILD file should use a legacy rule type for backwards compatibility, and half should use a non-legacy rule type, seems like a generally reasonable request, but currently requires global configuration of a plugin. This feature request is really about being able to make a more flexible building-block than map_kind
.
Is it possible to write multiple Gazelle extensions: one for RequiresNetwork
, one for Flaky
, and one for the rest. So you can have something like:
gazelle_binary(
name = "gazelle_bin",
# Gazelle runs the extensions below in order. Behavior may change if the order is changed.
languages = [
"//gazelle-extensions/java_flaky",
"//gazelle-extensions/java_network",
"//gazelle-extensions/java",
],
visibility = ["//visibility:public"],
)
//gazelle-extensions/java_flaky
and //gazelle-extensions/java_network
will generate specific kinds of Java targets when they see @Flaky
or @ RequiresNetwork
. Then //gazelle-extensions/java can skip the Java files that have been consumed by previous extensions, and only include the rest to regular Java targets, like this: https://github.com/bazelbuild/bazel-gazelle/blob/2b30bc443ea4494629527d6d72cd14243b22af5e/language/go/generate.go#L246-L258
//gazelle-extensions/java_flaky
and//gazelle-extensions/java_network
will generate specific kinds of Java targets when they see@Flaky
or@ RequiresNetwork
. Then //gazelle-extensions/java can skip the Java files that have been consumed by previous extensions
Is there a way for these to interplay nicely so that //gazelle-extensions/java
is still generating the deps
list for the targets identified by //gazelle-extensions/java_network
though?
It's not that we want to skip these targets come java generation, it's that we want to customise how they're rendered...
FWIW this limitation led us to writing/maintaining a lot of our own Gazelle plugins for Python, Proto, and many more. We didn't have the flexibility with pre-baked directives to make Gazelle generate our own rules properly.
@illicitonion Can you mock up a BUILD.bazel file for Java files with RequiresNetwork
. I wan to make sure I understand correctly
Absolutely:
load("//some:custom.bzl", "requires_network")
java_test(
name = "OrdinaryTest",
srcs = ["OrdinaryTest.java"],
deps = [
":helper",
"//junit",
],
)
requires_network(
java_test,
name = "RequiresNetworkTest",
srcs = ["RequiresNetworkTest.java"],
deps = [
":helper",
"//junit",
],
)
java_library(
name = "helper",
srcs = ["Helper.java"],
)
(We happen to use requires_network(java_test, ...)
but java_network_test(...)
would work equivalently)
Is there a way for these to interplay nicely so that //gazelle-extensions/java is still generating the deps list for the targets identified by //gazelle-extensions/java_network though?
Do you want OrdinaryTest
to have deps for RequiresNetworkTest
, even though OrdinaryTest.java
doesn't import them?
Do you want
OrdinaryTest
to have deps forRequiresNetworkTest
, even thoughOrdinaryTest.java
doesn't import them?
Ideally they would have independently gathered deps. But the logic for gathering the deps, and providing a resolver for them, lives firmly in the remit of the java extension.
//gazelle-extensions/java_network
can call functions in //gazelle-extensions/java
to gather and resolve deps, right?
I'm about to head out for the weekend, but will give a go at implementing these two tightly-coupled extensions shortly - thanks for the feedback!
(I'd also be curious as to @vardaro's use cases - maybe they could describe their use-cases some?)
I put together an example of two tightly coupled languages, and the result really wasn't very nice; some of the edge-cases that I ran into that are super fiddly:
language.NewLanguage()
is called, it has no context passed to it. (The Java resolver, for instance, is stateful, so we can't just make a new one, we need to share one with the Java backend). This works fine, but is not a nice pattern to be putting in place. And if we end up doing this for multiple languages, our plugin would need to multiplex based on generated target kind (original target kind though - map_kind
would make this harder) to one of several resolvers depending on what plugin should have generated the original target.Also, needing to parse each Java file twice (which is relatively expensive), or build a Java indexer cache shared by multiple processes, rather than just indexing once, isn't ideal.
For now, I'll just implement the extension I need using flags (to be repo-global) rather than with config directives (which would be more flexible), but I really do think we should make loads be able to refer to config here.
For now, I'll just implement the extension I need using flags (to be repo-global) rather than with config directives
Oh actually, this doesn't quite work out because we don't have https://github.com/bazelbuild/bazel-gazelle/pull/1274 - still, that at least is hopefully a simple PR to get merged! I'll try to put together some test for it soon.
when language.NewLanguage() is called
Is it possible for the Java plugin to implement a stateless function like ResolveGo
: https://github.com/bazelbuild/bazel-gazelle/blob/2b30bc443ea4494629527d6d72cd14243b22af5e/language/go/resolve.go#L120
That way, you can call it without creating a Language
instance.
re-implement the logic to decide whether to merge directories or not when generating rules.
Can you put the logic in a stateless function too?
Is it possible for the Java plugin to implement a stateless function
The Java plugin discovers packages by indexing, so fundamentally needs to store that indexing state somewhere so that the resolver can be driven by it. We could only make this stateless by removing some of the flexibility/functionality of the plugin, which would make it much less appealing to use (particularly as a migration tool).
Can you put the logic in a stateless function too?
We could, but at that point we're making an incredibly tight coupling between these plugins - that's going to make it fairly hard to evolve functionality.
Can I step back and check where the objection to adding Config
to these functions is? Is it just because changing the API is inconvenient (which I grant it is!), or is there also some technical objection to these functions having access to Config
? I'm not personally seeing the reason that adding Config
here would be bad, but I do definitely see that the API break is inconvenient. Is there a technical reason I'm missing?
so fundamentally needs to store that indexing state somewhere
ResolveGo
takes the index as an argument
Is it just because changing the API is inconvenient
Yeah, it means all Gazelle plugins are broken, but I think per directory configuration for Kinds()
and Loads()
is a useful feature. @achew22 @jayconrod What do you think?
Background / Feature Request
Currently, gazelle has exactly one place where config is allowed to affect per-build-file generated loads, and that's using
map_kind
.There are use-cases in practice for being able to do this more flexibly from arbitrary language plugins. For instance, say in Java you wanted to generate
java_test
targets most of the time, but if a test is marked as flaky (perhaps by annotating its class with an@Flaky
annotation, as suggested in https://github.com/bazelbuild/bazel-gazelle/pull/1293#issuecomment-1171525421), you want to generate aflaky_java_test
target, or if a test is marked as requiring network access (again, noted by annotation), you want to either generatenetwork_using_java_test
target.In particular, this may not be a static global property of a plugin; for a shared common Java plugin, different teams may have different conventions requiring custom mappings (e.g. they may define their own target types they want to generate for). In a multi-team monorepo, these may even be per-directory conventions.
In the Java gazelle plugin, we wish to make a flexible configuration mechanism, where someone can in a BUILD file write something like:
And to be able to customise what rules are generated as a result.
The Problem
Unfortunately, a
language.Language
must statically declare up-front all of the kinds/loads it may generate. Languages can be configured globally using flags, but don't have any way of varying the kinds/loads they generate based on the contents of BUILD files, which means teams can't make their own local choices.Proposed Solution
Ideally,
language.Language.Kinds()
andlanguage.Language.Loads()
would take arguments:c *config.Config, rel *string
to allow per-directory configuring to affect the output ofKinds()
andLoads()
.But this is a breaking change! There are a couple of approaches I can reasonably imagine:
language.Language
implementation isn't terribly complex. But it is churn.Loads()
/Kinds()
, sniff whether the other interface is implemented, and if so, call that instead.I'd prefer 1, but understand if it's too drastic. But in the long-term, I think we should converge on passing
Config
to these functions.I'd also be open to any other ways of achieving this same aim, but this was the cleanest I could work out.
Sample implementation
I put together a sample implementation in https://github.com/illicitonion/bazel-gazelle/commits/config-dependent-loader (just the top commit) which works for my aims; it includes an integration test showing the key behaviour in
tests/diff_mode_with_config
. I'd be happy to put this together as a formal PR if there's no better approach.