[ 
https://issues.apache.org/jira/browse/COLLECTIONS-776?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=17419514#comment-17419514
 ] 

ckurz commented on COLLECTIONS-776:
-----------------------------------

I believe I found the bug, the class 
{{org.apache.commons.collections4.map.PassiveExpiringMap}} does not provide an 
appropriate set when the Method 
{{org.apache.commons.collections4.map.PassiveExpiringMap#keySet}} is called 
which reflects the intended behavior. Similar methods probably have similar 
problems.

Does this need support? Lets look into the Documentation 
({{java.util.SortedMap#keySet}}):
{quote}[...] The set is backed by the map, so changes to the map are reflected 
in the set, and vice-versa. [...] The set supports element removal, which 
removes the corresponding mapping from the map, via the Iterator.remove, 
Set.remove, removeAll, retainAll, and clear operations. [...]
{quote}
It seems to me that the class is not in line with the documentation since it 
possible to get a key-set which holds keys which are will not be retrievable 
from the map. See code example below.

Fix:
I suggest we return a Set decorated which has the same functionality as the 
{{PassiveExpiringMap}} so something like a {{PassiveExpiringSet}} if such a 
thing exists. We should also add this functionality for the 
{{PassiveExpiringMap#values}} method and similar cases.
 One side note, this will probably get a little bit messy, there is some 
mutability involved which will fail in case of the multithreading and 
concurrency. Also keeping a key-set of a map in sync with the actual map 
probaply isnt the best idea :), even though I have some ideas how to adress 
this. 

 
The following code reflects a bit more the problem I try to point out. It 
actualy occurs in line 18 (final Set<String> keySet = m.keySet();):
{code:java}
import org.apache.commons.collections4.map.PassiveExpiringMap;

import java.text.SimpleDateFormat;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.TimeUnit;

public class Test {
    private static final SimpleDateFormat SDF = new 
SimpleDateFormat("HH:mm:ss");

    public static void main(String[] args) throws InterruptedException {
        final PassiveExpiringMap<String, Object> m = new 
PassiveExpiringMap<>(3, TimeUnit.SECONDS);
        final String key = UUID.randomUUID().toString();
        m.put(key, "");

        final Set<String> keySet = m.keySet();
        final Instant before = Instant.now();
        while (Instant.now().isBefore(before.plusSeconds(5))) {
            logWithTimestamp("Keys in wrapped map: " + String.join(", ", 
keySet));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException ignored) {
            }
        }
    }

    private static void logWithTimestamp(String string) {
        final Date resultdate = new Date(System.currentTimeMillis());
        System.out.println(Thread.currentThread().getName() + " [" + 
SDF.format(resultdate) + "] " + string);
    }
}
{code}

Any thoughts?
Any

> Wrapping PassiveExpiringMap in a SynchonizedMap breaks expiration
> -----------------------------------------------------------------
>
>                 Key: COLLECTIONS-776
>                 URL: https://issues.apache.org/jira/browse/COLLECTIONS-776
>             Project: Commons Collections
>          Issue Type: Bug
>          Components: Collection
>    Affects Versions: 4.4
>         Environment: Java 8
> Built on mac OS Catalina 10.15 using gradle 5.2.1 in Intellij
>            Reporter: Aidan Sadowski
>            Priority: Major
>
> The documentation for PassiveExpiringMap says "If you wish to use this map 
> from multiple threads concurrently, you must use appropriate synchronization. 
> The simplest approach is to wrap this map using 
> [{{Collections.synchronizedMap(Map)}}|https://docs.oracle.com/javase/7/docs/api/java/util/Collections.html?is-external=true#synchronizedMap-java.util.Map-].";
> However, wrapping a PassiveExpiringMap in a Collections.synchronizedMap seems 
> to break the PassiveExpiringMap's expiration. Specifically, the operation 
> passiveExirpringMap.keySet() no longer removes expired entries prior to 
> returning (which it should, according to the PassiveExpiringMap doc).
> I wrote this simple program to prove it. It puts a key into two 
> PassiveExpiringMaps with the same expiration policy of 15 seconds, one of 
> which is wrapped in a SyncronizedMap. Then it loops through the maps, calling 
> keySet() on them. The entry disappears from the unwrapped map after 15 
> seconds, but NOT the wrapped map! Code and log output below.
>  
> {code:java}
> import java.text.SimpleDateFormat;
> import java.util.Collections;
> import java.util.Date;
> import java.util.Map;
> import java.util.Set;
> import java.util.UUID;
> import java.util.concurrent.TimeUnit;
> import org.apache.commons.collections4.map.PassiveExpiringMap;
> public class Main {
>     // unwrapped
>     static Map<String, String> unwrappedMap;
>     // wrapped
>     static Map<String, String> wrappedMap;
>     public static void main(String[] args) throws InterruptedException {
>         unwrappedMap = new PassiveExpiringMap<>(15, TimeUnit.SECONDS);
>         wrappedMap = Collections.synchronizedMap(new PassiveExpiringMap<>(15, 
> TimeUnit.SECONDS));
>         // Put something in the maps
>         String key = UUID.randomUUID().toString();
>         logWithTimestamp("Putting key " + key);
>         unwrappedMap.put(key, "");
>         wrappedMap.put(key, "");
>         // Check the map until the key has expired, sleeping 1 second between 
> checks
>         while (true) {
>             Set<String> keysInUnwrappedMap = unwrappedMap.keySet();
>             Set<String> keysInWrappedMap = wrappedMap.keySet();
>             logWithTimestamp("Keys in unwrapped map: " + String.join(", ", 
> keysInUnwrappedMap));
>             logWithTimestamp("Keys in wrapped map: " + String.join(", ", 
> keysInWrappedMap));
>             Thread.sleep(1000);
>         }
>     }
>     private static SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
>     private static void logWithTimestamp(String string) {
>         Date resultdate = new Date(System.currentTimeMillis());
>         System.out.println("[" + sdf.format(resultdate) + "] " + string);
>     }
> }
> {code}
> {code:java}
> [10:02:41] Putting key fce9d058-8f13-4e6d-814e-90e03f575678
> [10:02:41] Keys in unwrapped map: fce9d058-8f13-4e6d-814e-90e03f575678
> [10:02:41] Keys in wrapped map: fce9d058-8f13-4e6d-814e-90e03f575678
> [10:02:42] Keys in unwrapped map: fce9d058-8f13-4e6d-814e-90e03f575678
> [10:02:42] Keys in wrapped map: fce9d058-8f13-4e6d-814e-90e03f575678
> [10:02:43] Keys in unwrapped map: fce9d058-8f13-4e6d-814e-90e03f575678
> [10:02:43] Keys in wrapped map: fce9d058-8f13-4e6d-814e-90e03f575678
> [10:02:44] Keys in unwrapped map: fce9d058-8f13-4e6d-814e-90e03f575678
> [10:02:44] Keys in wrapped map: fce9d058-8f13-4e6d-814e-90e03f575678
> [10:02:45] Keys in unwrapped map: fce9d058-8f13-4e6d-814e-90e03f575678
> [10:02:45] Keys in wrapped map: fce9d058-8f13-4e6d-814e-90e03f575678
> [10:02:46] Keys in unwrapped map: fce9d058-8f13-4e6d-814e-90e03f575678
> [10:02:46] Keys in wrapped map: fce9d058-8f13-4e6d-814e-90e03f575678
> [10:02:47] Keys in unwrapped map: fce9d058-8f13-4e6d-814e-90e03f575678
> [10:02:47] Keys in wrapped map: fce9d058-8f13-4e6d-814e-90e03f575678
> [10:02:48] Keys in unwrapped map: fce9d058-8f13-4e6d-814e-90e03f575678
> [10:02:48] Keys in wrapped map: fce9d058-8f13-4e6d-814e-90e03f575678
> [10:02:49] Keys in unwrapped map: fce9d058-8f13-4e6d-814e-90e03f575678
> [10:02:49] Keys in wrapped map: fce9d058-8f13-4e6d-814e-90e03f575678
> [10:02:50] Keys in unwrapped map: fce9d058-8f13-4e6d-814e-90e03f575678
> [10:02:50] Keys in wrapped map: fce9d058-8f13-4e6d-814e-90e03f575678
> [10:02:51] Keys in unwrapped map: fce9d058-8f13-4e6d-814e-90e03f575678
> [10:02:51] Keys in wrapped map: fce9d058-8f13-4e6d-814e-90e03f575678
> [10:02:52] Keys in unwrapped map: fce9d058-8f13-4e6d-814e-90e03f575678
> [10:02:52] Keys in wrapped map: fce9d058-8f13-4e6d-814e-90e03f575678
> [10:02:53] Keys in unwrapped map: fce9d058-8f13-4e6d-814e-90e03f575678
> [10:02:53] Keys in wrapped map: fce9d058-8f13-4e6d-814e-90e03f575678
> [10:02:54] Keys in unwrapped map: fce9d058-8f13-4e6d-814e-90e03f575678
> [10:02:54] Keys in wrapped map: fce9d058-8f13-4e6d-814e-90e03f575678
> [10:02:55] Keys in unwrapped map: fce9d058-8f13-4e6d-814e-90e03f575678
> [10:02:55] Keys in wrapped map: fce9d058-8f13-4e6d-814e-90e03f575678
> [10:02:56] Keys in unwrapped map: 
> [10:02:56] Keys in wrapped map: fce9d058-8f13-4e6d-814e-90e03f575678
> [10:02:57] Keys in unwrapped map: 
> [10:02:57] Keys in wrapped map: fce9d058-8f13-4e6d-814e-90e03f575678
> [10:02:58] Keys in unwrapped map: 
> [10:02:58] Keys in wrapped map: fce9d058-8f13-4e6d-814e-90e03f575678
> [10:02:59] Keys in unwrapped map: 
> [10:02:59] Keys in wrapped map: fce9d058-8f13-4e6d-814e-90e03f575678
> {code}



--
This message was sent by Atlassian Jira
(v8.3.4#803005)

Reply via email to