Quellcode durchsuchen

Fix concurrent modification in UpdateManager.

If we add or remove components while an update check is ongoing
we used to throw a CME. This could happen either when the client
is first starting and in the process of adding components, or
if some listener decided to add/remove components as they were
being checked.

Change-Id: I200a3b4804bd7e815a4f053ca596b41f7c4c4c24
Fixes-issue: CLIENT-404
Reviewed-on: http://gerrit.dmdirc.com/2795
Automatic-Compile: DMDirc Build Manager
Reviewed-by: Greg Holmes <greg@dmdirc.com>
tags/0.8rc1
Chris Smith vor 10 Jahren
Ursprung
Commit
a63f255f47

+ 43
- 32
src/com/dmdirc/updater/manager/UpdateManagerImpl.java Datei anzeigen

@@ -56,36 +56,31 @@ import lombok.extern.slf4j.Slf4j;
56 56
 public class UpdateManagerImpl implements UpdateManager {
57 57
 
58 58
     /** Collection of known update checking strategies. */
59
-    private final List<UpdateCheckStrategy> checkers
60
-            = new CopyOnWriteArrayList<UpdateCheckStrategy>();
59
+    private final List<UpdateCheckStrategy> checkers = new CopyOnWriteArrayList<>();
61 60
 
62 61
     /** Collection of known update retrieval strategies. */
63
-    private final List<UpdateRetrievalStategy> retrievers
64
-            = new CopyOnWriteArrayList<UpdateRetrievalStategy>();
62
+    private final List<UpdateRetrievalStategy> retrievers = new CopyOnWriteArrayList<>();
65 63
 
66 64
     /** Collection of known update installation strategies. */
67
-    private final List<UpdateInstallationStrategy> installers
68
-            = new CopyOnWriteArrayList<UpdateInstallationStrategy>();
65
+    private final List<UpdateInstallationStrategy> installers = new CopyOnWriteArrayList<>();
69 66
 
70
-    /** Map of known component names to their components. */
71
-    private final Map<String, UpdateComponent> components
72
-            = new HashMap<String, UpdateComponent>();
67
+    /** Map of known component names to their components. Guarded by {@link #componentsLock}. */
68
+    private final Map<String, UpdateComponent> components = new HashMap<>();
73 69
 
74 70
     /** Listener used to proxy retrieval events. */
75
-    private final UpdateRetrievalListener retrievalListener
76
-            = new RetrievalListener();
71
+    private final UpdateRetrievalListener retrievalListener = new RetrievalListener();
77 72
 
78 73
     /** Listener used to proxy setRetrievalResult events. */
79
-    private final UpdateInstallationListener installationListener
80
-            = new InstallListener();
74
+    private final UpdateInstallationListener installationListener = new InstallListener();
81 75
 
82 76
     /** Cache of update check results. */
83
-    private final Map<UpdateComponent, UpdateCheckResult> checkResults
84
-            = new HashMap<UpdateComponent, UpdateCheckResult>();
77
+    private final Map<UpdateComponent, UpdateCheckResult> checkResults = new HashMap<>();
85 78
 
86 79
     /** Cache of retrieval results. */
87
-    private final Map<UpdateComponent, UpdateRetrievalResult> retrievalResults
88
-            = new HashMap<UpdateComponent, UpdateRetrievalResult>();
80
+    private final Map<UpdateComponent, UpdateRetrievalResult> retrievalResults = new HashMap<>();
81
+
82
+    /** Lock for accessing {@link #components}. */
83
+    private final Object componentsLock = new Object();
89 84
 
90 85
     /** Executor to use to schedule retrieval and installation jobs. */
91 86
     private final Executor executor;
@@ -125,14 +120,19 @@ public class UpdateManagerImpl implements UpdateManager {
125 120
     @Override
126 121
     public void addComponent(final UpdateComponent component) {
127 122
         log.trace("Adding new component: {}", component);
128
-        this.components.put(component.getName(), component);
123
+        synchronized (componentsLock) {
124
+            this.components.put(component.getName(), component);
125
+        }
129 126
     }
130 127
 
131 128
     /** {@inheritDoc} */
132 129
     @Override
133 130
     public void removeComponent(final UpdateComponent component) {
134 131
         log.trace("Removing component: {}", component);
135
-        this.components.remove(component.getName());
132
+        synchronized (componentsLock) {
133
+            this.components.remove(component.getName());
134
+        }
135
+
136 136
         this.checkResults.remove(component);
137 137
         this.retrievalResults.remove(component);
138 138
     }
@@ -140,7 +140,9 @@ public class UpdateManagerImpl implements UpdateManager {
140 140
     /** {@inheritDoc} */
141 141
     @Override
142 142
     public Collection<UpdateComponent> getComponents() {
143
-        return Collections.unmodifiableCollection(this.components.values());
143
+        synchronized (componentsLock) {
144
+            return Collections.unmodifiableCollection(this.components.values());
145
+        }
144 146
     }
145 147
 
146 148
     /** {@inheritDoc} */
@@ -152,27 +154,36 @@ public class UpdateManagerImpl implements UpdateManager {
152 154
     /** {@inheritDoc} */
153 155
     @Override
154 156
     public void checkForUpdates() {
155
-        final Collection<Map<UpdateComponent, UpdateCheckResult>> results
156
-                = new ArrayList<Map<UpdateComponent, UpdateCheckResult>>();
157
+        final Collection<Map<UpdateComponent, UpdateCheckResult>> results = new ArrayList<>();
157 158
 
158 159
         log.info("Checking for updates for {} components using {} strategies",
159 160
                 components.size(), checkers.size());
160 161
         log.trace("Components: {}", components);
161 162
         log.trace("Strategies: {}", checkers);
162 163
 
163
-        final List<UpdateComponent> enabledComponents
164
-                = new ArrayList<UpdateComponent>(components.size());
165
-
166
-        for (UpdateComponent component : components.values()) {
167
-            if (policy.canCheck(component)) {
168
-                fireUpdateStatusChanged(component, UpdateStatus.CHECKING, 0);
169
-                enabledComponents.add(component);
170
-            } else {
171
-                log.debug("Checking for updates for {} denied by policy", component.getName());
172
-                fireUpdateStatusChanged(component, UpdateStatus.CHECKING_NOT_PERMITTED, 0);
164
+        final List<UpdateComponent> enabledComponents = new ArrayList<>(components.size());
165
+        final List<UpdateComponent> disabledComponents = new ArrayList<>(components.size());
166
+
167
+        synchronized (componentsLock) {
168
+            for (UpdateComponent component : components.values()) {
169
+                if (policy.canCheck(component)) {
170
+                    enabledComponents.add(component);
171
+                } else {
172
+                    log.debug("Checking for updates for {} denied by policy", component.getName());
173
+                    disabledComponents.add(component);
174
+                }
173 175
             }
174 176
         }
175 177
 
178
+        // Fire the listeners now we don't care about concurrent modifications.
179
+        for (UpdateComponent component : enabledComponents) {
180
+            fireUpdateStatusChanged(component, UpdateStatus.CHECKING, 0);
181
+        }
182
+
183
+        for (UpdateComponent component : disabledComponents) {
184
+            fireUpdateStatusChanged(component, UpdateStatus.CHECKING_NOT_PERMITTED, 0);
185
+        }
186
+
176 187
         for (UpdateCheckStrategy strategy : checkers) {
177 188
             results.add(strategy.checkForUpdates(enabledComponents));
178 189
         }

+ 84
- 0
test/com/dmdirc/updater/manager/UpdateManagerImplTest.java Datei anzeigen

@@ -0,0 +1,84 @@
1
+/*
2
+ * Copyright (c) 2006-2013 DMDirc Developers
3
+ *
4
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ * of this software and associated documentation files (the "Software"), to deal
6
+ * in the Software without restriction, including without limitation the rights
7
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ * copies of the Software, and to permit persons to whom the Software is
9
+ * furnished to do so, subject to the following conditions:
10
+ *
11
+ * The above copyright notice and this permission notice shall be included in
12
+ * all copies or substantial portions of the Software.
13
+ *
14
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ * SOFTWARE.
21
+ */
22
+
23
+package com.dmdirc.updater.manager;
24
+
25
+import com.dmdirc.updater.UpdateComponent;
26
+import com.dmdirc.updater.checking.CheckResultConsolidator;
27
+
28
+import java.util.concurrent.Executor;
29
+
30
+import lombok.extern.slf4j.Slf4j;
31
+import org.junit.Before;
32
+import org.junit.Test;
33
+import org.junit.runner.RunWith;
34
+import org.mockito.Mock;
35
+import org.mockito.invocation.InvocationOnMock;
36
+import org.mockito.runners.MockitoJUnitRunner;
37
+import org.mockito.stubbing.Answer;
38
+
39
+import static org.mockito.Mockito.*;
40
+
41
+@Slf4j
42
+@RunWith(MockitoJUnitRunner.class)
43
+public class UpdateManagerImplTest {
44
+
45
+    @Mock private Executor executor;
46
+    @Mock private CheckResultConsolidator checkResultConsolidator;
47
+    @Mock private UpdateComponentPolicy updateComponentPolicy;
48
+    @Mock private UpdateComponent component1;
49
+    @Mock private UpdateComponent component2;
50
+    @Mock private UpdateComponent component3;
51
+    @Mock private UpdateStatusListener statusListener;
52
+    private UpdateManagerImpl manager;
53
+
54
+    @Before
55
+    public void setup() {
56
+        manager = new UpdateManagerImpl(executor, checkResultConsolidator, updateComponentPolicy);
57
+        when(component1.getName()).thenReturn("test1");
58
+        when(component2.getName()).thenReturn("test2");
59
+        when(component3.getName()).thenReturn("test3");
60
+    }
61
+
62
+    @Test
63
+    public void testRemovingComponentInStatusChangeListener() {
64
+        manager.addComponent(component1);
65
+        manager.addComponent(component2);
66
+
67
+        manager.addUpdateStatusListener(statusListener);
68
+
69
+        doAnswer(new Answer<Void>() {
70
+            @Override
71
+            public Void answer(final InvocationOnMock invocation) throws Throwable {
72
+                // Add a new component from the manager while it's checking for updates.
73
+                // This potentially causes a CME, see CLIENT-404.
74
+                manager.addComponent(component3);
75
+                return null;
76
+            }
77
+        }).when(statusListener).updateStatusChanged(component1, UpdateStatus.CHECKING, 0);
78
+
79
+        when(updateComponentPolicy.canCheck(any(UpdateComponent.class))).thenReturn(true);
80
+
81
+        manager.checkForUpdates();
82
+    }
83
+
84
+}

Laden…
Abbrechen
Speichern