CLI: Close subcommands in MultiCommand#28954
Conversation
… properly Changes done to override close method and call close on subcommands. Closes elastic#28953
rjernst
left a comment
There was a problem hiding this comment.
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.
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.
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.
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.
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.
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.
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.
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
| } | ||
|
|
||
| @Override | ||
| public void close() throws IOException, RuntimeException { |
There was a problem hiding this comment.
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.
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
| 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.
use expectThrows here instead of the try catch
| } | ||
|
|
||
| public void testClose() throws Exception { | ||
| Command spySubCommand1 = spy(new DummySubCommand()); |
There was a problem hiding this comment.
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.
| } | ||
|
|
||
| public void testCloseWhenSubCommandCloseThrowsException() throws Exception { | ||
| Command subCommand1 = mock(DummySubCommand.class); |
There was a problem hiding this comment.
same comment about not really needing mockito
|
@elasticmachine Test this, please. |
|
|
||
| static class DummyMultiCommand extends MultiCommand { | ||
|
|
||
| AtomicBoolean closed = new AtomicBoolean(); |
There was a problem hiding this comment.
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.
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.
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(); |
| } | ||
| DummySubCommand(boolean throwsExceptionOnClose) { | ||
| super("A dummy subcommand", () -> {}); | ||
| this.throwsExceptionOnClose.compareAndSet(false, throwsExceptionOnClose); |
There was a problem hiding this comment.
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.
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.
for me must have been closed points me in the wrong direction; I'd use should have been or was not closed
rjernst
left a comment
There was a problem hiding this comment.
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.
Typically we use == false for negations (much easier to spot when glancing through some code).
There was a problem hiding this comment.
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.
Shouldn't this be "DummyMultiCommand already closed"?
| throw new IOException(); | ||
| } else { | ||
| if (!this.closed.compareAndSet(false, true)) { | ||
| throw new IOException("DummySubCommand closed"); |
| 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.
fyi, () -> multiCommand.close() can be multiCommand:close
|
|
||
| @Override | ||
| public void close() throws IOException { | ||
| if (throwsExceptionOnClose) { |
There was a problem hiding this comment.
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. |
jaymode
left a comment
There was a problem hiding this comment.
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.
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.
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.