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
Null policy #252
Comments
Same issue. I would like to feature-toggle a circuit breaker for roll out, but I can't have an empty list of policies. Can't really have |
Why don't you wrap Failsafe in a minimal interface of your own? Instead of relying on Failsafe directly everywhere, your code mainly depends on your own abstraction. And then you have a single place to do stuff like this. No need to wait for a library to provide you this. |
That's a good overall idea, but an investment cost I can't pay now to sell to team/pm the need of using failsafe (or any other stability framework). That's why I was going for a feature toggle solution, so they have confidence in merging. |
Honestly, I'd say it's the other way around. Binding to a 3rd-party library that you might not want to bind to or not having resilience built in both sound like something that one can't afford not to have. |
The problem of using a library is out of question and I'd prefer not to sidetrack the discussion to this topic.
The idea is to prove the concept with one simple resource, then generalize. If I start with a big change, it'll be harder to justify. |
The issue description mentions
This raises some questions: Would this no-op FailsafeExecutor call If not, then you're asking for an implementation of public final class FailsafeAllowingEmptyPolicyList {
public static <R, P extends Policy<R>> FailsafeExecutor<R> with(List<? extends Policy<R>> policies) {
if (policies.isEmpty())
return new NoPoliciesFailsafeExecutor<R>();
// Using this instead preserves FailsafeExecutor semantics on the returned value:
// return Failsafe.with(alwaysSuccessfulPolicy());
else
return Failsafe.with(policies);
}
// ... similarly for with(P... policies)
static class NoPoliciesFailsafeExecutor<R> extends FailsafeExecutor<R> {
// ... pass dummy policy to super constructor
// ... reimplement most (all?) methods, mostly trivially
}
Policy<R> alwaysSuccessfulPolicy() ... // e.g., handleIf(e -> false)
} But it would be hard to convince me (and here I'm addressing both @robertvazan and @hkupty) to use a new facility that could at any point return an object that doesn't meet the contract of its interface. (There are valid cases where one might break the contract of an interface deliberately, of course, but I don't think this is good example.) Using the commented-out return statement above instead of the incomplete Or (responding more to @hkupty here) you could use a configuration method taking a boolean argument that uses either the passed in policies or the single always-successful policy, depending on the value of that boolean. See below for how that might look. If the only thing your users will ever do is synchronous get/run, and they don't need the full interface MinimalFailsafeExecutor<R> {
<T extends R> T get(CheckedSupplier<T> supplier) throws FailsafeException;
void run(CheckedRunnable runnable) throws FailsafeException;
}
...
public final class MinimalFailsafe {
static class Builder {
private final boolean enabled;
Builder(boolean enabled) { this.enabled = enabled }
public <R, P extends Policy<R>> MinimalFailsafeExecutor<R> with(List<? extends Policy<R>> policies) {
if (enabled) {
FailsafeExecutor<R> fsExec = Failsafe.with(policies);
return new MinimalFailsafeExecutor<R>() {
... get(CheckedSupplier supplier) { ... fsExec.get(supplier) ... }
... run(CheckedRunnable runnable) { ... fsExec.run(runnable) ... }
};
} else {
return new MinimalFailsafeExecutor<R>() {
... get(CheckedSupplier supplier) { ... supplier.get() ... }
... run(CheckedRunnable runnable) { ... runnable.run() ... }
};
}
}
}
public static Builder enabled(boolean enabled) { return new Builder(enabled); }
public static <R, P extends Policy<R>> MinimalFailsafeExecutor<R> with(List<? extends Policy<R>> policies) {
return Builder.enabled(true).with(policies);
}
} Then your users can write the following, and disable the Failsafe machinery with a boolean: MinimalFailsafeExecutor<String> mfsExec = MinimalFailsafe.enabled(circuitBreakerEnabled).with(circuitBreaker);
...
String result = mfsExec.get(this::apiMethod); That last call is nearly equivalent to Later, once users are comfortable, if you decide that you need the full |
I agree with you. To be honest, I was surprised to find out that an empty list of policies was not accepted (which would essentially mean no-op), but I understand there might be implementation implications. That'd be preferred over a policy that does nothing IMO. I didn't mean to make this more than it really was: if I can configure enablement of policies through config, not a single enabled shouldn't break the application. I'm ok with the library not wanting to provide a semantically incorrect policy. This is a bit different and sounds worthy debating it... |
@Tembrel Just to clarify my original post, nobody is asking for breaking the API contract. Doing so would actually reduce usefulness of the no-op implementation. The tiny performance overhead of forwarding calls through All we are asking for is simplicity. The alternative you proposed is not simple, at least not as simple as I think your objections (i.e. performance and rendering some features useless) apply to any API that offers some sort of null implementation, for example |
I probably confused things unnecessarily by responding to both @robertvazan and @hkupty within one long comment. So here I go with another long comment. 😉 In responding to @robertvazan's issue description, that first code snippet is complicated because it's trying to demonstrate two things:
@hkupty seemed to be asking for something slightly different, a facility for bypassing Failsafe entirely based on a boolean variable, and complexity of the later code snippets is in sketching a way to achieve this without changing Failsafe. My point was that in both cases you can get what you want without changing Failsafe, if you really want it. I was also trying to hint that maybe you shouldn't really want it. There's not much point to a null Yes, it's awkward that Failsafe can't enforce the precondition that the policy list be non-empty at compile time, leading to run-time surprises. (Incidentally, why isn't it OT rant (that should probably be its own issue)Does anyone really need to build lists of policies in advance just to pass them into Wouldn't it be nicer to have @jhalterman has elsewhere (where?) expressed concern that it's hard to find method names that convey the composition of policies clearly, but I think |
If I may add my 2ct to this as well... I ran into the Exception as well, when I was starting with the library... Simplest approach, no vararg in with()... bam. The important question is really, how important is the ability to have an empty list / default policy for productive use... I think it might be useful for testing in contrast to deployment or even, as mentioned above, to have some checkboxes in a configuration to toggle the Policies... When I ran into that, I just used a quick-n-dirty solution: Retry with only one attempt. This seems to be the closest we get to the "empty" Policies... I've put together a simple example, that illustrates the problem with the custom wrapper / interface (which in general seems very reasonable - see below). import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import net.jodah.failsafe.Failsafe;
import net.jodah.failsafe.RetryPolicy;
public class FailsafeNoPolicy {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Runnable helloTask = () -> System.out.println("Hello World @ " + Thread.currentThread());
Runnable booomTask = () -> {throw new RuntimeException("Booom!");};
Runnable task = helloTask;
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<?> future;
if(true){
future = executor.submit(task); // just Future
} else {
future = Failsafe.with(new RetryPolicy<>().withMaxAttempts(1))
.with(executor)
.runAsync(task::run); // cancellable (!) Completable Future
}
executor.shutdown();
future.get(); // nothing fancy with CompletionStage possible...
}
} The explicit distinction between Failsafe / no Failsafe is good for readability - but not for the interface. If I would create an interface / facade to wrap this distinction, I could only provide Therefore, the general idea to wrap a library in a custom interface is very good, but even then I would want the CompletableFuture, which is way more labour-intensive, if it shall be cancellable... So yes, I think an Executor, that just executes and does nothing else - like my workaround |
Any preference for how to go about this? Some options:
I tend to like the straightforward simplicity of option 2. |
I think it's better to preserve the option of having FailsafeExecutor be an interface or abstract class, which option 2 precludes. So if I have to choose between the two, I think option 1 is better. |
Interesting point @Tembrel. What do you think that might be useful for? I wonder if there's a better option then |
It would allow you the option of using alternative implementations based on the provided policies, rather than forcing a single implementation to serve for all. More generally, if you don't have to have a public constructor, why make one part of the API? How about
|
+1 for the first option. It is consistent with the non-empty with-calls; otherwise one wouldn'd find this option as easy, as you would need to consult another class as usual ( Both options seem reasonable, using an empty |
Providing a public constructor pins down a specific implementation without adding anything that can't be provided by a static factory method. API design best practices argue against adding dependencies unnecessarily. Once you've promised the availability of a public constructor, you can't take it back. If |
@Tembrel Seeing your suggestion in another thread that |
No, better to fail at compile time, you're right.
…On Fri, Aug 6, 2021, 5:36 PM Jonathan Halterman ***@***.***> wrote:
@Tembrel <https://github.com/Tembrel> Seeing your suggestion in another
thread that with should require one policy made sense. I recently added a
with method that does this: 940f6b7
<940f6b7>.
Did you have in mind a reason why it's worth keeping but deprecating the no
arg with() method? I realize removing it would cause a compile error but
actually using it should yield a runtime exception.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#252 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AABZ5SWIYF67WBUMD73C2PTT3RIVTANCNFSM4NP4XZGA>
.
Triage notifications on the go with GitHub Mobile for iOS
<https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675>
or Android
<https://play.google.com/store/apps/details?id=com.github.android&utm_campaign=notification-email>
.
|
Is there a way to get null/noop version of
FailsafeExecutor
and perhaps even of the individual policies? I would like to add a requirement to passFailsafeExecutor
to some APIs, but I have to make sure this feature can be easily disabled by submitting some kind of null/noopFailsafeExecutor
that just executes everything as if the code was executed directly.I see that
Failsafe.with()
will throwIllegalArgumentException
if I give it an empty list of policies. I could probably find a workaround, for example by calling.handleIf(e -> false)
onRetryPolicy
. But is there a clean, concise solution with minimal overhead?The text was updated successfully, but these errors were encountered: