8373452: DataFormat threading and API issues - #2197
8373452: DataFormat threading and API issues#2197andy-goryachev-oracle wants to merge 7 commits into
Conversation
|
👋 Welcome back angorya! A progress list of the required criteria for merging this PR into |
|
❗ This change is not yet ready to be integrated. |
|
The total number of required reviews for this PR has been set to 2 based on the presence of this label: |
Webrevs
|
nlisker
left a comment
There was a problem hiding this comment.
Added some suggestions, but haven't done a serious review.
| throw new IllegalArgumentException("DataFormat '" + id + | ||
| "' already exists."); | ||
| if (ids == null) { | ||
| this.identifier = Collections.<String>emptySet(); |
There was a problem hiding this comment.
| this.identifier = Collections.<String>emptySet(); | |
| this.identifier = Set.of() |
| this.identifier = Collections.<String>emptySet(); | ||
| // don't care about registry | ||
| } else { | ||
| this.identifier = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(ids))); |
There was a problem hiding this comment.
| this.identifier = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(ids))); | |
| this.identifier = Set.of(ids); |
This also throws on nulls (but NPE, not IAE; you can rethrow). It also throws on duplicates which we need to do anyway.
| if (id == null) { | ||
| throw new IllegalArgumentException("DataFormat id must not be null."); | ||
| } |
There was a problem hiding this comment.
| if (id == null) { | |
| throw new IllegalArgumentException("DataFormat id must not be null."); | |
| } |
There was a problem hiding this comment.
the proposed code is no different? in any case, throwing an NPE might be better in this case.
There was a problem hiding this comment.
I'm proposing to remove this since the null check in ids was done with Set.of(ids). It's unreachable code (unless ids is changed in a background thread maybe, but I don't think this is a real concern, and you can work with identifier instead anyway).
There was a problem hiding this comment.
I agree (I think I got confused by the github diff in the conversation).
| } else if (obj instanceof DataFormat f) { | ||
| return identifier.equals(f.identifier); | ||
| } | ||
|
|
||
| return false; |
There was a problem hiding this comment.
| } else if (obj instanceof DataFormat f) { | |
| return identifier.equals(f.identifier); | |
| } | |
| return false; | |
| } | |
| return (obj instanceof DataFormat df && identifier.equals(df.identifier); |
| DataFormat f = registry.get(id); | ||
| if (f != null) { | ||
| if (!this.identifier.equals(f.identifier)) { | ||
| throw new IllegalArgumentException("DataFormat '" + id + "' already exists."); | ||
| } | ||
| isNew = false; | ||
| } |
There was a problem hiding this comment.
| DataFormat f = registry.get(id); | |
| if (f != null) { | |
| if (!this.identifier.equals(f.identifier)) { | |
| throw new IllegalArgumentException("DataFormat '" + id + "' already exists."); | |
| } | |
| isNew = false; | |
| } | |
| boolean noneExist = Collections.disjoint(this.identifie, registry.keySet()); |
There was a problem hiding this comment.
my version is better as it avoids scanning twice
There was a problem hiding this comment.
This claim might be too strong. From a performance perspective, the sizes of the collections are very small (a DataFormat will realistically have at most 3 ids and the total number of ids/mime-types that exist in the wild is probably in the dozens only), so performance is not really a consideration. However, if we do look at it, your implementation iterates over the array, which is O(n), with a hashed get which is O(1). Collections.disjoint is optimized to the given collections - it iterates over the smaller collection, which is O(n), and checks contains which is O(1). So I don't think your code gives better performance.
From a readability perspective, what your code says is "for each id, find a data format that uses it" (and if the DataFormat shares 2 or more ids, you will find and compare the same DataFormat twice or more). disjoint is clearer: "does any of the new ids exist already?"
In fact, I'm not sure why you're throwing only when not all the identifiers are the same, that is, why you specified "if one of the given ids is already assigned to another DataFormat with a different set of ids". If some are the same it's an exception, but if all of them are the same then it's not? Maybe I'm missing something.
There was a problem hiding this comment.
my version is faster, but the difference is so minute it's irrelevant. I can use disjoint() here.
If some are the same it's an exception, but if all of them are the same then it's not?
This is the essence of the change. We are to allow multiple DataFormat instances if they are equal, so as to avoid the unnecessary exceptions.
The reason we can't allow disjoint formats is because the lookupMimeType() logic will break, and that would be a more drastic breaking change.
| if (id == null) { | ||
| throw new IllegalArgumentException("DataFormat id must not be null."); | ||
| } |
There was a problem hiding this comment.
I'm proposing to remove this since the null check in ids was done with Set.of(ids). It's unreachable code (unless ids is changed in a background thread maybe, but I don't think this is a real concern, and you can work with identifier instead anyway).
| "' already exists."); | ||
| if (ids == null) { | ||
| this.identifier = Set.of(); | ||
| // don't care about registry |
There was a problem hiding this comment.
You can return here and avoid the big else branch.
There was a problem hiding this comment.
I like big else branches :-)
| DataFormat f = registry.get(id); | ||
| if (f != null) { | ||
| if (!this.identifier.equals(f.identifier)) { | ||
| throw new IllegalArgumentException("DataFormat '" + id + "' already exists."); | ||
| } | ||
| isNew = false; | ||
| } |
There was a problem hiding this comment.
This claim might be too strong. From a performance perspective, the sizes of the collections are very small (a DataFormat will realistically have at most 3 ids and the total number of ids/mime-types that exist in the wild is probably in the dozens only), so performance is not really a consideration. However, if we do look at it, your implementation iterates over the array, which is O(n), with a hashed get which is O(1). Collections.disjoint is optimized to the given collections - it iterates over the smaller collection, which is O(n), and checks contains which is O(1). So I don't think your code gives better performance.
From a readability perspective, what your code says is "for each id, find a data format that uses it" (and if the DataFormat shares 2 or more ids, you will find and compare the same DataFormat twice or more). disjoint is clearer: "does any of the new ids exist already?"
In fact, I'm not sure why you're throwing only when not all the identifiers are the same, that is, why you specified "if one of the given ids is already assigned to another DataFormat with a different set of ids". If some are the same it's an exception, but if all of them are the same then it's not? Maybe I'm missing something.
We've got this A better solution would be to disallow multi-id formats and require the single format id to be a valid mime type, but that would be a more drastic change. |
|
re: disjoint(): sorry, I don't want to deal with peculiarities of it (empty set etc.). |
| if (lookupMimeType(id) != null) { | ||
| throw new IllegalArgumentException("DataFormat '" + id + | ||
| "' already exists."); | ||
| if (ids == null) { |
There was a problem hiding this comment.
| if (ids == null) { | |
| if (ids == null || ids.length == 0) { |
I think we can shortcut an empty id list.
What peculiarities? We are talking about sets of It's a suggestions to make the code clearer, ignore it if you want. As it is, it takes quite some effort to understand what the constructor does and it's mostly complexity for irrelevant optimizations (the registry will not be larger than a few dozen entries, and this is a stretch probably). I'd value readability here over saving a microsecond and write: public DataFormat(String... ids) {
if (ids == null || ids.length == 0) {
identifier = Set.of();
return;
}
identifier = Set.of(ids);
if (registry.containsValue(this)) {
// all ids exist in an existing data format - no need to change registry
return;
}
if (!Collections.disjoint(identifier, registry.keySet())) {
// existing ids can't be overridden - throw
throw new IllegalArgumentException("An id already exists");
}
// no existing ids - add
identifier.forEach(id -> registry.put(id, this));
}I also find the name |
| @Test | ||
| public void nullArrayIsAllowedForCompatibilityReasons() { | ||
| String[] mimes = null; | ||
| new DataFormat(mimes); |
There was a problem hiding this comment.
| new DataFormat(mimes); | |
| assertDoesNotThrow(() -> new DataFormat(mimes)); |
| assertThrows(NullPointerException.class, () -> { | ||
| String mime = null; | ||
| new DataFormat(mime); | ||
| }); |
There was a problem hiding this comment.
| assertThrows(NullPointerException.class, () -> { | |
| String mime = null; | |
| new DataFormat(mime); | |
| }); | |
| String mime = null; | |
| assertThrows(NullPointerException.class, () -> { | |
| new DataFormat(mime); | |
| }); |
Recommendation is to always have one statement in the assertThrows / assertDoesNotThrows
|
re: disjoint
And you are right, it's rather unlikely for any DataFormat (except FILES) to contain more than one element, so we are basically bikeshedding. The main question is whether this PR solves the problem sufficiently, or should we ban multi-id data formats altogether? I could not find an origin or any use of |
|
@andy-goryachev-oracle This pull request has been inactive for more than 4 weeks and will be automatically closed if another 4 weeks passes without any activity. To avoid this, simply issue a |
|
/touch |
|
@andy-goryachev-oracle The pull request is being re-evaluated and the inactivity timeout has been reset. |
Summary
This PR changes the behavior of
DataFormatby allowing multiple instances that contain the same set of mime types (ids).Problem
There seems to be several issues with DataFormat API and implementation discovered during review of the
Clipboard-related code:static DataFormat::lookupMimeType(String)is not thread safe: while iterating over previously registered entries in theDATA_FORMAT_LISTanother thread might create a new instance (DataFormat L227)public DataFormat(String...)constructor might throw anIllegalArgumentExceptionif one of the given mime types is already assigned to anotherDataFormat. The origin of this requirement is unclear, but one possible issue I can see is if the application has two libraries that both attempt to create aDataFormatfor let's say"text/css". Then, depending on the timing or the exact code path, an exception will be thrown for which the library(-ies) might not be prepared. The constructor is also not thread safe.To avoid a situation mentioned in bullet 2, a developer would typically call
lookupMimeType()to obtain the already registered instance, followed by a constructor call if such an instance has not been found. An example of such code can be seen in webkit/UIClientImpl:299 - but even then, despite that two-step process being synchronized, the code might still fail if some other library or the application attempts to create a new instance of DataFormat, since the constructor itself is not synchronized.DataFormat(new String[] { null })is allowed but makes no sense!The current implementation uses the
WeakReferenceQueuewhich theoretically might, under certain conditions, allow the application to create mismatchedDataFormats.Why do we need to have the registry of previously created instances? Unclear. My theory is that the DataFormat allows to have multiple mime-types (ids) - example being
DataFormat.FILES = new DataFormat("application/x-java-file-list", "java.file-list");- and the registry was added to prevent creation of aDataFormatwith just one id for some reason.Also, I could not find the origin of the multi-id requirement, or the origin of the
java.file-listid itself (it is not a valid mime type).Solution
The proposed solution is to relax the constraint on the constructor to allow multiple instances with the same set of mime types (ids). This might require changing the application code from identity compare (
==) to.equals().The change also adds synchronization on the registry inside the constructor and
DataFormat::lookupMimeType(String).Additionally, the constructor checks for
nullids, throwing anIllegalArgumentException. (Quite possibly, we might want to disallownew DataFormat((String[])null)which is currently allowed.Compatibility Impact
The applications that relied on identity check (
if(dataFormat == DataFormat.FILES)) must be changed to use theequals()method. This might be a rare problem because most of the time the applications declare custom format as a static constant and use it to access the clipboard / dragboard data.The constructor throws a
NullPointerExceptionif any of the ids is null.Alternatives
One alternative is to deprecate the multi-id constructor and only allow one mime type per
DataFormat, also removing thejava.file-listid. We could then still keepSet<String> DataFormat.getIdentifiers()which will always return a set with one element.Notes
Progress
Issue
Reviewing
Using
gitCheckout this PR locally:
$ git fetch https://git.openjdk.org/jfx.git pull/2197/head:pull/2197$ git checkout pull/2197Update a local copy of the PR:
$ git checkout pull/2197$ git pull https://git.openjdk.org/jfx.git pull/2197/headUsing Skara CLI tools
Checkout this PR locally:
$ git pr checkout 2197View PR using the GUI difftool:
$ git pr show -t 2197Using diff file
Download this PR as a diff file:
https://git.openjdk.org/jfx/pull/2197.diff
Using Webrev
Link to Webrev Comment