Skip to content
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

Merged
merged 25 commits into from
Nov 30, 2024

Conversation

venfernand
Copy link
Contributor

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]

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;
}
Copy link
Contributor

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()
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

much nicer — thank you!

src/freenet/support/MultiValueTable.java Show resolved Hide resolved
@ArneBab
Copy link
Contributor

ArneBab commented Apr 12, 2023

@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.

@ArneBab
Copy link
Contributor

ArneBab commented May 23, 2023

@venfernand will you add the shims to preserve the existing API?

@venfernand
Copy link
Contributor Author

@venfernand will you add the shims to preserve the existing API?

Sorry for being too distructive.
I restored all previous public API in MultiValueTable and marked some methods as @Deprecated to speed up transition to newer methods.

@venfernand venfernand requested a review from ArneBab October 8, 2023 08:15
src/freenet/clients/http/FProxyFetchTracker.java Outdated Show resolved Hide resolved
return progress;
} else
if(logMINOR) Logger.minor(this, "Skipping "+progress);
} else if (logMINOR) {
Copy link
Contributor

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.

Copy link
Contributor Author

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.*;
Copy link
Contributor

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.

Copy link
Contributor Author

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
Copy link
Contributor

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.)

Copy link
Contributor Author

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) {
Copy link
Contributor

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?

Copy link
Contributor Author

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() {
Copy link
Contributor

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. :)

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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?

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().
@venfernand
Copy link
Contributor Author

I added some more fixes.

@venfernand venfernand requested a review from Bombe October 20, 2023 20:30
});
}

public void putAll(K key, Collection<V> elements) {
Copy link
Contributor

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>.

Copy link
Contributor Author

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();
Copy link
Contributor

@bertm bertm Oct 20, 2023

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.

Copy link
Contributor Author

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) {
Copy link
Contributor

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.

Copy link
Contributor Author

@venfernand venfernand Oct 20, 2023

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?

Copy link
Contributor

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.

Copy link
Contributor Author

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<>();
Copy link
Contributor

@bertm bertm Oct 20, 2023

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.

Copy link
Contributor Author

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);
Copy link
Contributor

@bertm bertm Oct 20, 2023

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.

Copy link
Contributor Author

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.

Comment on lines 139 to 142
List<V> list = this.table.get(key);
if (list == null || list.isEmpty()) {
return Collections.emptyList();
}
Copy link
Contributor

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());

Copy link
Contributor Author

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.

Comment on lines 197 to 199
if (downloads == null) {
return null;
}
Copy link
Contributor

@bertm bertm Oct 20, 2023

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed

Comment on lines 101 to 103
if (fetchList == null) {
return null;
}
Copy link
Contributor

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.

Copy link
Contributor Author

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();
Copy link
Contributor

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.

Copy link
Contributor Author

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);
Copy link
Contributor

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)

@ArneBab @Bombe thoughts on removing this method?

Copy link
Contributor Author

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.

Comment on lines 218 to 221
* @param key key mapping
*/
@Deprecated
public Enumeration<K> keys() {
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

venfernand and others added 8 commits October 21, 2023 03:00
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.
@ArneBab
Copy link
Contributor

ArneBab commented Nov 30, 2024

@bertm is this ready to be merged, now?

@venfernand can you merge next into your branch and resolve the conlicts?

Copy link
Contributor

@bertm bertm left a 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.

src/freenet/support/MultiValueTable.java Show resolved Hide resolved
List<V> result;
if (previousList == null) {
// FIXME: replace with List.of(v) when Java version baseline becomes >= 11
result = new ArrayList<>(1);
Copy link
Contributor

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());
Copy link
Contributor

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);.

@ArneBab ArneBab merged commit 1b1c950 into hyphanet:next Nov 30, 2024
1 check passed
@ArneBab
Copy link
Contributor

ArneBab commented Nov 30, 2024

merged after resolving conflicts. Waiting longer would only increase the amount of conflicts.

@ArneBab
Copy link
Contributor

ArneBab commented Nov 30, 2024

@venfernand thank you for your PR!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants