-
Notifications
You must be signed in to change notification settings - Fork 25k
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
CLI: Close subcommands in MultiCommand #28954
Conversation
… properly Changes done to override close method and call close on subcommands. Closes elastic#28953
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left a couple comments. Also, please reword the PR title to indicate what is changing, eg "CLI: Close subcommands in MultiCommand". And for future reference, there is no need to create an associated issue with a PR.
|
||
@Override | ||
public void close() throws IOException { | ||
if (subcommands.isEmpty()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think close()
is the place to be erroring if subcommands were never setup. This would already happen in execute
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed the review comment, by removing checks in the close method. Thank you.
if (subcommands.isEmpty()) { | ||
throw new IllegalStateException("No subcommands configured"); | ||
} | ||
for (Command command : subcommands.values()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use IOUtils.close()
instead of iterating over the subcommands, as this will ensure all subcommands are closed, even on failures.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi Ryan, Not sure but is IOUtils from org.apache.lucene.util package? and can CLI be dependent on the Lucene utils? Please suggest so I can add the dependency. Thank you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I forgot cli classes were moved out to their own jar. We definitely shouldn't add lucene core just to get IOUtils. But we do need to mimic the behavior here, otherwise one command failing to close could cause the others not to close.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure I will take it up and add similar behavior as in IOUtils#close. Thank you.
One more thing: please add a test for this to |
Pinging @elastic/es-core-infra |
throw new IllegalStateException("No subcommands configured"); | ||
} | ||
for (Command command : subcommands.values()) { | ||
if (command != null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If a command
is null
there is a problem; let us not be lenient like this.
Handling sub command close exceptions, so others can be closed and then rethrow exception. Adding unit tests. Closes elastic#28953
@@ -74,4 +76,48 @@ protected void execute(Terminal terminal, OptionSet options) throws Exception { | |||
} | |||
subcommand.mainWithoutErrorHandling(Arrays.copyOfRange(args, 1, args.length), terminal); | |||
} | |||
|
|||
@Override | |||
public void close() throws IOException, RuntimeException { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we should copy this here. I think if we are going to copy it, it should be available everywhere and replace the usage of the Lucene IOUtils
. I opened #29012.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, Jason. I was not sure if we needed other utils but looks like from your description of the PR there are others who use the same across source code.
Just one question, around can CLI be dependent on elasticsearch-core? In current project dependencies, it is not listed. I can update my change to depend on the internal IOUtils.
CLI now depends on elasticsearch-core, for IOUtils. Closes elastic#28953
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left a few comments about the testing. I also added the v6.3.0 label as this should be backported to 6.x.
Command spySubCommand2 = spy(new DummySubCommand()); | ||
multiCommand.subcommands.put("command1", subCommand1); | ||
multiCommand.subcommands.put("command2", spySubCommand2); | ||
IOException ioe = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use expectThrows
here instead of the try catch
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. Thank you.
@@ -102,4 +110,32 @@ public void testSubcommandArguments() throws Exception { | |||
assertFalse(output, output.contains("command1")); | |||
assertTrue(output, output.contains("Arguments: [foo, bar]")); | |||
} | |||
|
|||
public void testClose() throws Exception { | |||
Command spySubCommand1 = spy(new DummySubCommand()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like mockito when it is necessary but sometimes it isn't and I feel that way here. I'd suggest having an AtomicBoolean or AtomicInteger that gets modified in the close method of a anonymous Command class. Then you can assert on that value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, Thank you.
} | ||
|
||
public void testCloseWhenSubCommandCloseThrowsException() throws Exception { | ||
Command subCommand1 = mock(DummySubCommand.class); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same comment about not really needing mockito
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, Thank you.
@elasticmachine Test this, please. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left some more comments
public class MultiCommandTests extends CommandTestCase { | ||
|
||
static class DummyMultiCommand extends MultiCommand { | ||
|
||
AtomicBoolean closed = new AtomicBoolean(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
make this field final
and add a line after for readability
DummyMultiCommand() { | ||
super("A dummy multi command", () -> {}); | ||
} | ||
@Override |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a personal preference but given that we're adding to this class, I find it hard to read without empty lines between methods/constructor/member fields. Can you please add them?
} | ||
|
||
static class DummySubCommand extends Command { | ||
AtomicBoolean throwsExceptionOnClose = new AtomicBoolean(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why does this need to be an AtomicBoolean? I think it can just be a final boolean
} | ||
|
||
static class DummySubCommand extends Command { | ||
AtomicBoolean throwsExceptionOnClose = new AtomicBoolean(); | ||
AtomicBoolean closed = new AtomicBoolean(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
make this final
DummySubCommand() { | ||
super("A dummy subcommand", () -> {}); | ||
} | ||
DummySubCommand(boolean throwsExceptionOnClose) { | ||
super("A dummy subcommand", () -> {}); | ||
this.throwsExceptionOnClose.compareAndSet(false, throwsExceptionOnClose); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
when you use compareAndSet
you need to check the value returned by this method. In this case there is something really wrong if it returns false.
} | ||
|
||
public void testCloseWhenSubCommandCloseThrowsException() { | ||
boolean throwExceptionWhenClosed = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice to test the other scenarios or add some randomization to this test in terms of which subcommand(s) throws an exception:
final boolean command1Throws = randomBoolean();
final boolean command2Throws = randomBoolean();
...
assertTrue(command1Throws ^ subCommand1.closed.get());
assertTrue(command2Throws ^ subCommand2.closed.get());
multiCommand.subcommands.put("command1", subCommand1); | ||
multiCommand.subcommands.put("command2", subCommand2); | ||
multiCommand.close(); | ||
assertTrue("MultiCommand must have been closed when close method is invoked", multiCommand.closed.get()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for me must have been closed
points me in the wrong direction; I'd use should have been
or was not closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left a couple more comments. No need from me for another review if you agree with them all. Thanks for all the iterations!
@Override | ||
public void close() throws IOException { | ||
super.close(); | ||
if (!this.closed.compareAndSet(false, true)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typically we use == false
for negations (much easier to spot when glancing through some code).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, sure. Thank you. I will disable this in sonarLint which I use where it flags it as minor when == false
.
public void close() throws IOException { | ||
super.close(); | ||
if (!this.closed.compareAndSet(false, true)) { | ||
throw new IOException("DummyMultiCommand closed"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be "DummyMultiCommand already closed"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
throw new IOException(); | ||
} else { | ||
if (!this.closed.compareAndSet(false, true)) { | ||
throw new IOException("DummySubCommand closed"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comments here as above.
multiCommand.subcommands.put("command2", subCommand2); | ||
// verify exception is thrown, as well as other non failed sub-commands closed | ||
// properly. | ||
IOException ioe = expectThrows(IOException.class, () -> multiCommand.close()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fyi, () -> multiCommand.close()
can be multiCommand:close
|
||
@Override | ||
public void close() throws IOException { | ||
if (throwsExceptionOnClose) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you have this condition+throw separate from setting closed (maybe rename closed to closeCalled) then you don't need the difficult to read xor logic in the last test.
@elasticmachine Test this, please. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left a few minor comments, otherwise this LGTM. Once that's addressed and CI is green you should be good to merge this in!
public void close() throws IOException { | ||
super.close(); | ||
if (this.closed.compareAndSet(false, true) == false) { | ||
throw new IOException("DummyMultiCommand already closed"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO this is not really an issue with IO so IOException
doesn't make much sense; how about we change this to an IllegalStateException
?
@Override | ||
public void close() throws IOException { | ||
if (this.closeCalled.compareAndSet(false, true) == false) { | ||
throw new IOException("DummySubCommand already closed"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same thing here, how about using IllegalStateException
* es/master: (97 commits) Clarify requirements of strict date formats. (#29090) Clarify that dates are always rendered as strings. (#29093) Compilation fix for #29067 [Docs] Fix link to Grok patterns (#29088) Store offsets in index prefix fields when stored in the parent field (#29067) Fix starting on Windows from another drive (#29086) Use removeTask instead of finishTask in PersistentTasksClusterService (#29055) Added minimal docs for reindex api in java-api docs Allow overriding JVM options in Windows service (#29044) Clarify how to set compiler and runtime JDKs (#29101) CLI: Close subcommands in MultiCommand (#28954) TEST: write ops should execute under shard permit (#28966) [DOCS] Add X-Pack upgrade details (#29038) Revert "Improve error message for installing plugin (#28298)" Docs: HighLevelRestClient#exists (#29073) Validate regular expressions in dynamic templates. (#29013) [Tests] Fix GetResultTests and DocumentFieldTests failures (#29083) Reenable LiveVersionMapTests.testRamBytesUsed on Java 9. (#29063) Mute failing GetResultTests and DocumentFieldTests Improve error message for installing plugin (#28298) ...
* CLI Command: MultiCommand must close subcommands to release resources properly - Changes are done to override the close method and call close on subcommands using IOUtils#close - Unit Test Closes #28953
* es/6.x: (46 commits) Docs: Add note about missing mapping for doc values field (#29036) [DOCS] Removed 6.1.4, 6.2.2, and 6.2.3 coming tags Remove BytesArray and BytesReference usage from XContentFactory (#29151) Fix BWC issue for PreSyncedFlushResponse Add pluggable XContentBuilder writers and human readable writers (#29120) Add unreleased version 6.2.4 (#29171) Add unreleased version 6.1.5 (#29168) Add a note about using the `retry_failed` flag before accepting data loss (#29160) Fix typo in percolate-query.asciidoc (#29155) Add release notes for 6.1.4 and 6.2.3 Require HTTP::Tiny 0.070 for release notes script REST high-level client: add clear cache API (#28866) Relax remote check for bwc project checkouts (#28666) Set Java 9 checkstyle to depend on checkstyle conf (#28383) Docs: Add example of resetting index setting (#29048) Plugins: Fix module name conflict check for meta plugins (#29146) Build: Fix meta plugin bundled plugin names (#29147) Build: Simplify rest spec hack configuration (#29149) CLI: Close subcommands in MultiCommand (#28954) Build: Fix meta modules to not install as plugin in tests (#29150) ...
Changes are done in MultiCommand, to override the close method and calling close on each subcommand to release resources.