-
-
Notifications
You must be signed in to change notification settings - Fork 141
Ensure IonReader
instances created within IonFactory
are always resource-managed
#325
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
Conversation
Sounds reasonable to me. But I'd like to get a +1 from Ion maintainers. @mcliedtke @jobarr-amzn WDYT? |
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.
tgregg@ is much more an Ion expert than I, see the contributors breakdown for ion-java
. I'm happy to take a look though.
Thanks for doing this, Tyler! All comments optional, I tossed out a couple testing opinions you can take or leave as you like.
private enum InputSource { | ||
BYTE_ARRAY() { | ||
@Override | ||
IonParser newParser(IonFactory factory) throws IOException { | ||
return (IonParser) factory.createParser(BINARY_INT_0); | ||
} | ||
}, | ||
CHAR_ARRAY() { | ||
@Override | ||
IonParser newParser(IonFactory factory) throws IOException { | ||
return (IonParser) factory.createParser(TEXT_INT_0.toCharArray()); | ||
} | ||
}, | ||
READER() { | ||
@Override | ||
IonParser newParser(IonFactory factory) throws IOException { | ||
return (IonParser) factory.createParser(new StringReader(TEXT_INT_0)); | ||
} | ||
}, | ||
INPUT_STREAM() { | ||
@Override | ||
IonParser newParser(IonFactory factory) throws IOException { | ||
return (IonParser) factory.createParser(new ByteArrayInputStream(BINARY_INT_0)); | ||
} | ||
}, | ||
ION_READER() { | ||
@Override | ||
IonParser newParser(IonFactory factory) { | ||
return factory.createParser(factory.getIonSystem().newReader(BINARY_INT_0)); | ||
} | ||
}, | ||
ION_VALUE() { | ||
@Override | ||
IonParser newParser(IonFactory factory) { | ||
return factory.createParser(factory.getIonSystem().newInt(0)); | ||
} | ||
}; | ||
|
||
abstract IonParser newParser(IonFactory factory) throws IOException; | ||
} | ||
|
||
@Parameterized.Parameters(name = "{0}") | ||
public static InputSource[] parameters() { | ||
return InputSource.values(); | ||
} | ||
|
||
@Parameterized.Parameter | ||
public InputSource inputSource; | ||
|
||
@Test | ||
public void testIonParserIsResourceManaged() throws IOException { | ||
IonFactory factory = new IonFactory(); | ||
IonParser parser = inputSource.newParser(factory); | ||
// When the user provides an IonReader, it is not resource-managed, meaning that the user retains the | ||
// responsibility to close it. In all other cases, the IonReader is created internally, is resource-managed, | ||
// and is closed automatically in IonParser.close(). | ||
assertEquals(inputSource != InputSource.ION_READER, parser._ioContext.isResourceManaged()); | ||
assertTrue(IonReader.class.isAssignableFrom(parser._ioContext.contentReference().getRawContent().getClass())); | ||
parser.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.
private enum InputSource { | |
BYTE_ARRAY() { | |
@Override | |
IonParser newParser(IonFactory factory) throws IOException { | |
return (IonParser) factory.createParser(BINARY_INT_0); | |
} | |
}, | |
CHAR_ARRAY() { | |
@Override | |
IonParser newParser(IonFactory factory) throws IOException { | |
return (IonParser) factory.createParser(TEXT_INT_0.toCharArray()); | |
} | |
}, | |
READER() { | |
@Override | |
IonParser newParser(IonFactory factory) throws IOException { | |
return (IonParser) factory.createParser(new StringReader(TEXT_INT_0)); | |
} | |
}, | |
INPUT_STREAM() { | |
@Override | |
IonParser newParser(IonFactory factory) throws IOException { | |
return (IonParser) factory.createParser(new ByteArrayInputStream(BINARY_INT_0)); | |
} | |
}, | |
ION_READER() { | |
@Override | |
IonParser newParser(IonFactory factory) { | |
return factory.createParser(factory.getIonSystem().newReader(BINARY_INT_0)); | |
} | |
}, | |
ION_VALUE() { | |
@Override | |
IonParser newParser(IonFactory factory) { | |
return factory.createParser(factory.getIonSystem().newInt(0)); | |
} | |
}; | |
abstract IonParser newParser(IonFactory factory) throws IOException; | |
} | |
@Parameterized.Parameters(name = "{0}") | |
public static InputSource[] parameters() { | |
return InputSource.values(); | |
} | |
@Parameterized.Parameter | |
public InputSource inputSource; | |
@Test | |
public void testIonParserIsResourceManaged() throws IOException { | |
IonFactory factory = new IonFactory(); | |
IonParser parser = inputSource.newParser(factory); | |
// When the user provides an IonReader, it is not resource-managed, meaning that the user retains the | |
// responsibility to close it. In all other cases, the IonReader is created internally, is resource-managed, | |
// and is closed automatically in IonParser.close(). | |
assertEquals(inputSource != InputSource.ION_READER, parser._ioContext.isResourceManaged()); | |
assertTrue(IonReader.class.isAssignableFrom(parser._ioContext.contentReference().getRawContent().getClass())); | |
parser.close(); | |
} | |
@Test | |
public void byteArrayIsManaged() throws Throwable { | |
assertResourceManaged(true, parser(f -> f.createParser(BINARY_INT_0))); | |
} | |
@Test | |
public void charArrayIsManaged() throws Throwable { | |
assertResourceManaged(true, parser(f -> f.createParser(TEXT_INT_0.toCharArray()))); | |
} | |
@Test | |
public void readerIsManaged() throws Throwable { | |
assertResourceManaged(true, parser(f -> f.createParser(new StringReader(TEXT_INT_0)))); | |
} | |
@Test | |
public void inputStreamIsManaged() throws Throwable { | |
assertResourceManaged(true, parser(f -> f.createParser(new ByteArrayInputStream(BINARY_INT_0)))); | |
} | |
@Test | |
public void ionValueIsManaged() throws Throwable { | |
assertResourceManaged(true, parser(f -> f.createParser(f.getIonSystem().newInt(0)))); | |
} | |
@Test | |
public void ionReaderIsNotManaged() throws Throwable { | |
// When the user provides an IonReader, it is not resource-managed, meaning that the user retains the | |
// responsibility to close it. In all other cases, the IonReader is created internally, is resource-managed, | |
// and is closed automatically in IonParser.close(). | |
assertResourceManaged(false, parser(f -> f.createParser(f.getIonSystem().newReader(BINARY_INT_0)))); | |
} | |
private void assertResourceManaged(boolean expectResourceManaged, ThrowingSupplier<IonParser> supplier) | |
throws Throwable { | |
IonParser parser = supplier.get(); | |
Assertions.assertEquals(expectResourceManaged, parser._ioContext.isResourceManaged()); | |
Assertions.assertTrue(IonReader.class.isAssignableFrom(parser._ioContext.contentReference().getRawContent().getClass())); | |
parser.close(); | |
} | |
private interface ThrowingFunction<T,R> { | |
R apply(T t) throws Throwable; | |
} | |
private static ThrowingSupplier<IonParser> parser(ThrowingFunction<IonFactory, JsonParser> f) { | |
return () -> (IonParser) f.apply(new IonFactory()); | |
} |
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.
We can drop a dozen lines and the parameterization at the same time without increasing the maintenance burden of the test cases imo. ThrowingSupplier
and Assertions
here come from JUnit 5 (in use by jackson-core so it ought to be fine to bring in). If you want to stick on JUnit 4.13 then ThrowingSupplier
is another 3 lines.
If we really want parameterized tests then we can use Java 8's functional features to drop 20 lines from the enum and make it more readable:
enum InputSource {
BYTE_ARRAY(true, parser(f -> f.createParser(BINARY_INT_0))),
CHAR_ARRAY(true, parser(f -> f.createParser(TEXT_INT_0.toCharArray()))),
READER(true, parser(f -> f.createParser(new StringReader(TEXT_INT_0)))),
INPUT_STREAM(true, parser(f -> f.createParser(new ByteArrayInputStream(BINARY_INT_0)))),
ION_VALUE(true, parser(f -> f.createParser(f.getIonSystem().newInt(0)))),
ION_READER(false, parser(f -> f.createParser(f.getIonSystem().newReader(BINARY_INT_0)))),
;
private final boolean isResourceManaged;
private final ThrowingSupplier<IonParser> supplier;
InputSource(boolean isResourceManaged, ThrowingSupplier<IonParser> s) {
this.isResourceManaged = isResourceManaged;
this.supplier = s;
}
}
@ParameterizedTest
@EnumSource(InputSource.class)
public void testIonParserIsResourceManaged(InputSource inputSource) throws Throwable {
assertResourceManaged(inputSource.isResourceManaged, inputSource.supplier);
}
I tried out a few other patterns here but didn't really love any of them. I think non-parameterized is the best for this case. Your mileage may vary.
@jobarr-amzn Thanks! While we are discussing maintainers -- I'd be happy to add links to devs you think would qualify as co-authors for the Ion format module, from |
@tgregg Just let me know when you are happy with this and I will merge it -- timing is great since we still have time until 2.14. |
2e2836e
to
f3c2564
Compare
@cowtowncoder I incorporated @jobarr-amzn's feedback, so this is ready to merge from our perspective. Thanks for your help. Feel free to add me to the list of contributors you're compiling. |
Thank you @tgregg ! I did add you as a maintainer and happy to add anyone else who wants to volunteer. There isn't anything specific you need to do of course, it's more information so I know who to occasionally refer from issues et. Will now merge this. |
IonReader
instances created within IonFactory
are always resource-managed
Hmmh. I should have read this more closely... it does expose a problem b/w problem of closing (or not) of the underlying input source and Also, need to figure out how to merge to master/3.0 as there's significant refactoring. |
Was able to merge fine to master/3.0, with some tweaks, test passing. |
This is intended to fix the memory leak issue described in #306, superseding that pull request.
This solution ensures that
IonReader
instances created byIonFactory
are marked as "resource-managed", meaning that they are closed duringIonParser.close()
. Closing anIonReader
causes it to close any resources it creates (e.g. aGZIPInputStream
, as was the case in the original PR).Note:
IonFactory
/IonParser
currently ignores theStreamReadFeature.AUTO_CLOSE_SOURCE
feature, which allows users to opt-in to disabling closure of resources (e.g. anInputStream
) provided by the user. Before this change, user-providedInputStream
s andReader
s would not be closed byIonParser.close()
because theIonReader
created to wrap the resources was never closed. Now that thatIonReader
is closed, those user-provided resources will be closed transitively viaIonReader.close()
. Although this is technically a behavioral change, it seems unlikely to be harmful, as resuming parsing of a partially-consumed stream of Ion data without any context is only possible in specific circumstances. It seems far more likely that current users are either 1) manually closing these resources, in which case this change leads to a redundant close (not harmful in theInputStream
/Reader
implementations I'm aware of), or 2) forgetting to close these resources, which may leak memory, depending on the implementation. Option 2) is even more likely for users used toJsonFactory
, whereAUTO_CLOSE_SOURCE
is (as far as I can tell) on by default. Therefore, I conclude that the benefits of this change outweigh the risks; however, I'm open to other opinions.This solution isn't very aesthetically-pleasing, as it discards and replaces the
IOContext
passed in by theJsonFactory
superclass. This amounts to one redundantIOContext
instantiation percreateParser
call. This could probably be avoided, but it would require a larger refactor.The included unit test verifies that
IonReader
s are always marked as resource-managed except when provided directly by the user. The test assumes correct behavior ofIonParser.close()
; namely, that resource-managed resources that areCloseable
will be closed. Before this fix, theBYTE_ARRAY
,CHAR_ARRAY
,READER
, andINPUT_STREAM
cases failed.