-
Notifications
You must be signed in to change notification settings - Fork 210
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
Improve MultiValueTable #829
Improve MultiValueTable #829
Conversation
MultiValueTable previously used HashTable class internally and all access methods required to be synchronized. This solution was replaced with ConcurrentHashMap and CopyOnWriteArrayList, which may be accessed concurrently and with less synchronization. Also changed some methods, which used older collection APIs. Improved usages of MultiValueTable. Signed-off-by: Veniamin Fernandes <[email protected]>
} | ||
if (noFilter && download.filterData) { | ||
continue; | ||
} |
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.
nice change! (we need this at many places — but only where we actually change stuff to avoid breaking existing pull requests we cannot merge yet)
MultiValueTable<String, String> headers = MultiValueTable.from( | ||
"Location", | ||
directoryBrowserPath + params + "path=" + core.getDownloadsDir().getAbsolutePath() | ||
); |
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.
much nicer — thank you!
@venfernand thank you for your pull request! This makes the code much nicer. To ensure that this does not break plugins, it needs to preserve all existing API, so to be able to merge it, this needs shims for all previously existing public methods and constructors. |
@venfernand will you add the shims to preserve the existing API? |
Sorry for being too distructive. |
return progress; | ||
} else | ||
if(logMINOR) Logger.minor(this, "Skipping "+progress); | ||
} else if (logMINOR) { |
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.
As the first if
branch ends with a return
, we don’t need the else
here and get rid of that one level of indentation.
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 tried to improve this method even more.
import java.util.Locale; | ||
import java.util.StringJoiner; | ||
import java.util.TimeZone; | ||
import java.util.*; |
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 don’t use star imports, they can make this code break in the future.
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.
Expanded.
} | ||
|
||
// content may be cached | ||
// For privacy reasons, only static |
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.
Comments in the wrong order. 😄 (Also, put them into one line, that’s toally fine.)
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 think it was an outcome of some automatic refactoring or just regular blindness :)
removeAndGet(key); | ||
} | ||
|
||
public List<V> removeAndGet(K key) { |
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.
Do we actually need this method? For the same reason that we can’t simply remove methods we should also be wary about adding new public
methods, so if we don’t have an immediate use for it elsewhere in Fred, maybe add this as private
first, if at all?
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 may be used when atomic remove and retrieve is required, but you are right, there are no real usages at the moment.
I may be added quite easily if this will be required.
I removed it.
public final boolean hasMoreElements() { | ||
return global.hasMoreElements(); // || current.hasMoreElements(); | ||
} | ||
public Stream<V> 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.
I don’t think this method should return a Stream
, mostly because it seems to be rare for collections to return a Stream
from anywhere else but a .stream()
method. Also, if this returned a collection, you could make the elements()
method way shorter. :)
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 tried to return a lazy view, which does not allocate that much.
But i agree that this breaks expectations of callers and it is better to return a simpler type from there.
I changed it to return Collection<V>
and now it looks simpler.
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.
Oof, most of these tests are pretty horrible. 🙁 Coverage is pretty good, though, so they’ll do.
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.
Do you mean that there is no need to check such trivial places?
Or those preparations of test collections are not very clear?
…ariable is declared
Invert if condition with null check and remove one level of indentation. Add logging for the case when fetch context does not match. Improve the Javadoc.
This is a new method, which was added recently. There is no use case for it a the moment. It can be added back again if necessary.
Previous solution with java.util.stream.Stream used to return a lazy view of MultiValueTable values. Now this method returns a Collection, which makes it to be similar to java.util.Map.values().
I added some more fixes. |
}); | ||
} | ||
|
||
public void putAll(K key, Collection<V> elements) { |
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.
Collection<V>
put arguments should read Collection<? extends V>
instead, so it becomes possible to put a List<Foo>
into a MultiValueTable<?, Object>
.
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.
Pushed changes for putAll()
and from()
methods.
v.elements()); | ||
List<V> elements = getAllAsList(key); | ||
if (elements.isEmpty()) { | ||
return Collections.emptyEnumeration(); |
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 special case is unnecessary. An enumeration over an empty collection is equivalent to an empty enumeration.
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.
The only benefit of Collections.emptyEnumeration()
is that it returns same object instance all the time.
Otherwise, yes, it can be simpler.
Will make it to be one-liner then.
/** | ||
* Returns the first element for this key. | ||
*/ | ||
public V getFirst(K key) { |
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.
Now this is just a duplicate of get
. Is there any real need for extending the public API with this method? If so, please consider deprecating the method it replaces.
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.
New method looks more explicit for me.
Besides that there is no that big need and old method may be still used.
Which variant do you prefer more?
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.
getFirst
make way more sense indeed (not that any of the other methods of this class make much sense, but that's all hindsight)
We should not have 2 non-deprecated methods doing the exact same thing, because then things get confusing for developers - which one should you use? I'm all for getFirst
, but then let's deprecate get
and refer to getFirst
in the @deprecated
notice.
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.
Added more deprecation comments there.
table.put(key, v); | ||
this.table.compute(key, (k, list) -> { | ||
if (list == null) { | ||
list = new CopyOnWriteArrayList<>(); |
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 the CoW list? If we stick to unmodifiable lists instead (replace the list in the map, instead of altering its contents), the implementation becomes a whole lot easier to reason about (once you've read a value list, its contents remain stable forever) and you don't need to mess with wrapping in Collections.unmodifiableList()
on every read.
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.
CopyOnWriteArrayList
was picked to allow concurrent iteration and access to be safe.
It also was a continuation of the previous implementation, where underlying data structure was modified in place.
Immutable lists are really better here, but these require some more code to write.
Stable collections may also prevent from various concurrent modification bus.
I pushed new changes containing immutable lists instead of CopyOnWriteArrayList
.
if (list == null || list.isEmpty()) { | ||
return Collections.emptyList(); | ||
} | ||
return Collections.unmodifiableList(list); |
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 returns an unmodifiable read-through view of the underlying list. This means that when a new entry is inserted or an existing entry deleted in the table for this key, the change is reflected in this returned list. That is highly unexpected (and if intended, should be thoroughly documented), most likely undesired, and probably an unintended effect of choice of datastructure. See also my comment about the use of CoWList.
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 was also true for the Enumeration
objects, which were returned before.
I think that nobody ever used this like that and wanted any updates during iteration or later use.
I agree that it is really better to return "safe" and stable collections and iterators.
Now, when lists are truly immutable, then all returned values are also not modifiable.
Please see the new changes, it is probably better now.
List<V> list = this.table.get(key); | ||
if (list == null || list.isEmpty()) { | ||
return Collections.emptyList(); | ||
} |
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 could be reduced to a simple oneliner:
List<V> list = table.getOrDefault(key, Collections.emptyList());
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, the new implementation looks exactly like that.
When table holds immutable lists, then those may be returned as is.
if (downloads == null) { | ||
return 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.
Unreachable return, getAllAsList
never returns 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.
Removed
if (fetchList == null) { | ||
return 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.
Unreachable return; fetchList
is never 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.
Removed
return r; | ||
} | ||
} | ||
return getAllAsList(key).toArray(); |
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 breaks the (undocumented) return value semantics of getArray
: previously it would return null
when no values are present, currently it returns an empty array.
Please consider simulating the old behaviour and documenting it in the Javadoc.
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.
Restored the previous behavior and put notes into Javadoc
synchronized (table) { | ||
return table.get(key); | ||
} | ||
return getAllAsList(key); |
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 method used to return a Vector
(or null
if there was no value), now it always returns a List
. There are many methods that are implemented by Vector but not by List, so this is a breaking change (even though this behaviour was undocumented, as usual).
Additionally, this now returns an unmodifiable list where it would previously allow modifications. We might as well remove this method right away - we cannot in any reasonable way simulate previous behaviour using this new implementation, and no sane plugin should use it anyways (given its entirely undocumented nature).
I checked the following plugins to be sure (listing by Plugin-Main-Class):
de.saces.fnplugins.Shoeshop.ShoeshopPlugin
de.saces.fnplugins.SiteToolPlugin.SiteToolPlugin
freenetsearch.Plugin
keepalive.Plugin
net.pterodactylus.sone.main.SonePlugin
org.freenetproject.freemail.FreemailPlugin
plugins.CENO.Client.CENOClient
plugins.floghelper.FlogHelper
plugins.flophelper.FlogHelper
plugins.FMS.FMSPlugin
plugins.Freereader.Freereader
plugins.JSTUN.JSTUN
plugins.KeyUtils.KeyUtilsPlugin
plugins.Library.Main
plugins.MDNSDiscovery.MDNSDiscovery
plugins.N2NChat.core.N2NChatPlugin
plugins.ShareLink.Plugin
plugins.Sharesite.Plugin
plugins.ShareWiki.Plugin
plugins.ThawIndexBrowser.ThawIndexBrowser
plugins.UPnP2.UPnP2
plugins.UPnP.UPnP
plugins.WebOfTrust.WebOfTrust
None of them use anything from MultiValueTable except:
freenet.support.MultiValueTable#<init>() // the no-args constructor
freenet.support.MultiValueTable#put(java.lang.Object,java.lang.Object)
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 changed this back to return Vector
.
It may be removed at any time later.
* @param key key mapping | ||
*/ | ||
@Deprecated | ||
public Enumeration<K> keys() { |
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 method does not have a key
parameter even though one is specified in the Javadoc.
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.
Fixed
Co-authored-by: Bert Massop <[email protected]>
Previous solution containing CopyOnWriteArrayList had redundant Collections.unmodifiableList() wrapping for each read in getAllAsList(). It is better to make the underlying list to be immutable. This also provides stable results for callers if MultiValueTable is modified after elements are already returned to caller.
…ment their behavior Wrapped returned values from entrySet() and keySet() with unmodifiable views. Described more verbosely the properties of returned values.
This method previously used to return null when key is absent.
@bertm is this ready to be merged, now? @venfernand can you merge |
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.
There is still room for improvement, but this looks good enough to go.
List<V> result; | ||
if (previousList == null) { | ||
// FIXME: replace with List.of(v) when Java version baseline becomes >= 11 | ||
result = new ArrayList<>(1); |
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 could be optimized to return Collections.singletonList(value);
.
this.table.compute(key, (k, previousList) -> { | ||
List<V> result; | ||
if (previousList == null) { | ||
result = new ArrayList<>(elements.size()); |
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 could be optimized to return new ArrayList<>(elements);
.
merged after resolving conflicts. Waiting longer would only increase the amount of conflicts. |
@venfernand thank you for your PR! |
MultiValueTable previously used HashTable class internally and all access methods required to be synchronized. This solution was replaced with ConcurrentHashMap and CopyOnWriteArrayList, which may be accessed concurrently and with less synchronization.
Also changed some methods, which used older collection APIs.
Improved usages of MultiValueTable.
Signed-off-by: Veniamin Fernandes [email protected]