Skip to content

Commit 1bcfbe4

Browse files
committed
fix: dont freeze editor when closing editor with offline cluster
Freezing was caused by race conditions in the locks. Closing the editor disposes the editor resources. Disposing the editor resource closes the cluster connection. When closing the watch, a new resource state is retrieved, this then ended in the resource lock waiting for the connection timeout. Solved it by introducing a new "Disposed" state which is returned whenever an editor resource is disposed. No further state retrieval occurrs. Signed-off-by: Andre Dietisheim <[email protected]>
1 parent e8f76cf commit 1bcfbe4

File tree

8 files changed

+122
-20
lines changed

8 files changed

+122
-20
lines changed

src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResource.kt

+21-13
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata
2424
import io.fabric8.kubernetes.client.KubernetesClient
2525
import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil
2626
import io.fabric8.kubernetes.client.utils.Serialization
27+
import java.util.concurrent.atomic.AtomicBoolean
2728
import java.util.concurrent.locks.ReentrantLock
2829
import kotlin.concurrent.withLock
2930

@@ -39,7 +40,7 @@ open class EditorResource(
3940
) -> ClusterResource? =
4041
ClusterResource.Factory::create
4142
) {
42-
var disposed: Boolean = false
43+
var disposed: AtomicBoolean = AtomicBoolean(false)
4344
/** for testing purposes */
4445
private var state: EditorResourceState? = null
4546
/** for testing purposes */
@@ -94,6 +95,9 @@ open class EditorResource(
9495

9596
/** for testing purposes */
9697
open fun setState(state: EditorResourceState?) {
98+
if (disposed.get()) {
99+
return
100+
}
97101
resourceChangeMutex.withLock {
98102
this.state = state
99103
}
@@ -108,14 +112,18 @@ open class EditorResource(
108112
* @see [createState]
109113
*/
110114
fun getState(): EditorResourceState {
111-
return resourceChangeMutex.withLock {
112-
val existingState = this.state
113-
if (existingState == null) {
114-
val newState = createState(resource, null)
115-
setState(newState)
116-
newState
117-
} else {
118-
existingState
115+
return if (disposed.get()) {
116+
Disposed()
117+
} else {
118+
resourceChangeMutex.withLock {
119+
val existingState = this.state
120+
if (existingState == null) {
121+
val newState = createState(resource, null)
122+
setState(newState)
123+
newState
124+
} else {
125+
existingState
126+
}
119127
}
120128
}
121129
}
@@ -262,7 +270,7 @@ open class EditorResource(
262270
/** for testing purposes */
263271
protected open fun setLastPushedPulled(resource: HasMetadata?) {
264272
resourceChangeMutex.withLock {
265-
lastPushedPulled = resource
273+
this.lastPushedPulled = resource
266274
}
267275
}
268276

@@ -332,13 +340,13 @@ open class EditorResource(
332340
}
333341

334342
fun dispose() {
335-
if (disposed) {
343+
if (disposed.get()) {
336344
return
337345
}
338-
this.disposed = true
339-
clusterResource?.close()
346+
this.disposed.set(true)
340347
setLastPushedPulled(null)
341348
setResourceVersion(null)
349+
clusterResource?.close()
342350
}
343351

344352
private fun createClusterResource(resource: HasMetadata): ClusterResource? {

src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResourceState.kt

+14
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,20 @@ class Error(val title: String, val message: String? = null): EditorResourceState
6363
}
6464
}
6565

66+
class Disposed: EditorResourceState() {
67+
68+
override fun equals(other: Any?): Boolean {
69+
if (this === other) {
70+
return true
71+
}
72+
return other is Disposed
73+
}
74+
75+
override fun hashCode(): Int {
76+
return Objects.hash()
77+
}
78+
}
79+
6680
open class Identical: EditorResourceState()
6781

6882
abstract class Different(val exists: Boolean, val isOutdatedVersion: Boolean): EditorResourceState() {

src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResources.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ open class EditorResources(
5959
// remove editor resource for old resource that doesn't exist anymore
6060
!new.contains(identifier)
6161
// or editor resource that was disposed (ex. change in namespace, context)
62-
|| editorResource.disposed
62+
|| editorResource.disposed.get()
6363
}
6464
resources.keys.removeAll(toRemove.keys)
6565
toRemove.values.forEach { editorResource ->

src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/ResourceWatch.kt

+8-4
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@
1111
package com.redhat.devtools.intellij.kubernetes.model
1212

1313
import com.intellij.openapi.diagnostic.logger
14+
import com.intellij.util.concurrency.AppExecutorUtil
1415
import io.fabric8.kubernetes.api.model.HasMetadata
1516
import io.fabric8.kubernetes.client.KubernetesClientException
1617
import io.fabric8.kubernetes.client.Watch
1718
import io.fabric8.kubernetes.client.Watcher
1819
import io.fabric8.kubernetes.client.WatcherException
20+
import org.jetbrains.concurrency.runAsync
1921
import java.util.*
2022
import java.util.concurrent.BlockingDeque
2123
import java.util.concurrent.ConcurrentHashMap
@@ -29,15 +31,15 @@ import java.util.concurrent.LinkedBlockingDeque
2931
*/
3032
open class ResourceWatch<T>(
3133
protected val watchOperations: BlockingDeque<WatchOperation<*>> = LinkedBlockingDeque(),
32-
watchOperationsRunner: Runnable = WatchOperationsRunner(watchOperations)
34+
watchOperationsRunner: Runnable = WatchOperationsRunner(watchOperations),
35+
private val executor: (runnable: Runnable) -> Unit = { runnable -> AppExecutorUtil.getAppExecutorService().execute(runnable) }
3336
) {
3437
companion object {
3538
@JvmField val WATCH_OPERATION_ENQUEUED: Watch = Watch { }
3639
}
3740

3841
protected open val watches: ConcurrentHashMap<T, Watch?> = ConcurrentHashMap()
39-
private val executor: ExecutorService = Executors.newWorkStealingPool(20)
40-
private val thread = executor.submit(watchOperationsRunner)
42+
private val thread = executor.invoke(watchOperationsRunner)
4143

4244
open fun watchAll(
4345
toWatch: Collection<Pair<T, (watcher: Watcher<in HasMetadata>) -> Watch?>>,
@@ -91,7 +93,9 @@ open class ResourceWatch<T>(
9193
}
9294

9395
fun close() {
94-
closeAll(watches.entries.toList())
96+
executor.invoke {
97+
closeAll(watches.entries.toList())
98+
}
9599
}
96100

97101
private fun closeAll(entries: Collection<MutableMap.MutableEntry<T, Watch?>>) {

src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResourceStateTest.kt

+46
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ class EditorResourceStateTest {
114114
assertThat(Error("yoda", "is a green gnome")).isNotEqualTo(Error("yoda", "is a jedi"))
115115
assertThat(Error("obiwan", "is a jedi")).isNotEqualTo(Error("yoda", "is a jedi"))
116116

117+
assertThat(Error("obiwan", "is a jedi")).isNotEqualTo(Disposed())
118+
117119
assertThat(Error("yoda", "is a jedi")).isNotEqualTo(Identical())
118120

119121
assertThat(Error("yoda", "is a jedi")).isNotEqualTo(Modified(true, true))
@@ -133,12 +135,41 @@ class EditorResourceStateTest {
133135
assertThat(Error("yoda", "is a jedi")).isNotEqualTo(Pulled())
134136
}
135137

138+
@Test
139+
fun `Disposed is not equal to all the other states`() {
140+
// given
141+
// when
142+
assertThat(Disposed()).isNotEqualTo(Error("yoda", "is a jedi"))
143+
144+
assertThat(Disposed()).isEqualTo(Disposed())
145+
146+
assertThat(Disposed()).isNotEqualTo(Identical())
147+
148+
assertThat(Disposed()).isNotEqualTo(Modified(true, true))
149+
assertThat(Disposed()).isNotEqualTo(Modified(true, false))
150+
assertThat(Disposed()).isNotEqualTo(Modified(false, false))
151+
152+
assertThat(Disposed()).isNotEqualTo(DeletedOnCluster())
153+
154+
assertThat(Disposed()).isNotEqualTo(Outdated())
155+
156+
assertThat(Disposed()).isNotEqualTo(Created(true))
157+
assertThat(Disposed()).isNotEqualTo(Created(false))
158+
159+
assertThat(Disposed()).isNotEqualTo(Updated(true))
160+
assertThat(Disposed()).isNotEqualTo(Updated(false))
161+
162+
assertThat(Disposed()).isNotEqualTo(Pulled())
163+
}
164+
136165
@Test
137166
fun `Identical is not equal to all the other states`() {
138167
// given
139168
// when
140169
assertThat(Identical()).isNotEqualTo(Error("yoda", "is a jedi"))
141170

171+
assertThat(Identical()).isNotEqualTo(Disposed())
172+
142173
assertThat(Identical()).isEqualTo(Identical())
143174

144175
assertThat(Identical()).isNotEqualTo(Modified(true, true))
@@ -167,6 +198,10 @@ class EditorResourceStateTest {
167198
assertThat(Modified(false, false)).isNotEqualTo(Error("yoda", "is a jedi"))
168199
assertThat(Modified(false, true)).isNotEqualTo(Error("yoda", "is a jedi"))
169200

201+
assertThat(Modified(true, true)).isNotEqualTo(Disposed())
202+
assertThat(Modified(false, true)).isNotEqualTo(Disposed())
203+
assertThat(Modified(false, false)).isNotEqualTo(Disposed())
204+
170205
assertThat(Modified(true, true)).isNotEqualTo(Identical())
171206
assertThat(Modified(true, false)).isNotEqualTo(Identical())
172207
assertThat(Modified(false, false)).isNotEqualTo(Identical())
@@ -217,6 +252,8 @@ class EditorResourceStateTest {
217252
// when
218253
assertThat(DeletedOnCluster()).isNotEqualTo(Error("darth vader", "is on the dark side"))
219254

255+
assertThat(DeletedOnCluster()).isNotEqualTo(Disposed())
256+
220257
assertThat(DeletedOnCluster()).isNotEqualTo(Identical())
221258

222259
assertThat(DeletedOnCluster()).isNotEqualTo(Modified(true, true))
@@ -243,6 +280,8 @@ class EditorResourceStateTest {
243280
// when
244281
assertThat(Outdated()).isNotEqualTo(Error("obiwan", "is a jedi"))
245282

283+
assertThat(Outdated()).isNotEqualTo(Disposed())
284+
246285
assertThat(Outdated()).isNotEqualTo(Identical())
247286

248287
assertThat(Outdated()).isNotEqualTo(Modified(true, true))
@@ -270,6 +309,8 @@ class EditorResourceStateTest {
270309
assertThat(Created(true)).isNotEqualTo(Error("darth vader", "is on the dark side"))
271310
assertThat(Created(false)).isNotEqualTo(Error("darth vader", "is on the dark side"))
272311

312+
assertThat(Created(false)).isNotEqualTo(Disposed())
313+
273314
assertThat(Created(true)).isNotEqualTo(Identical())
274315
assertThat(Created(false)).isNotEqualTo(Identical())
275316

@@ -309,6 +350,9 @@ class EditorResourceStateTest {
309350
assertThat(Updated(true)).isNotEqualTo(Error("darth vader", "is on the dark side"))
310351
assertThat(Updated(false)).isNotEqualTo(Error("darth vader", "is on the dark side"))
311352

353+
assertThat(Updated(true)).isNotEqualTo(Disposed())
354+
assertThat(Updated(false)).isNotEqualTo(Disposed())
355+
312356
assertThat(Updated(true)).isNotEqualTo(Identical())
313357
assertThat(Updated(false)).isNotEqualTo(Identical())
314358

@@ -347,6 +391,8 @@ class EditorResourceStateTest {
347391
// when
348392
assertThat(Pulled()).isNotEqualTo(Error("darth vader", "is on the dark side"))
349393

394+
assertThat(Pulled()).isNotEqualTo(Disposed())
395+
350396
assertThat(Pulled()).isNotEqualTo(Identical())
351397

352398
assertThat(Pulled()).isNotEqualTo(Modified(true, true))

src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResourceTest.kt

+23
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,29 @@ class EditorResourceTest {
513513
assertThat(state).isInstanceOf(Error::class.java)
514514
}
515515

516+
@Test
517+
fun `#getState should return Disposed if resource is disposed`() {
518+
// given
519+
val editorResource = createEditorResource(POD2)
520+
editorResource.disposed.set(true)
521+
val state = editorResource.getState()
522+
// then
523+
assertThat(state).isInstanceOf(Disposed::class.java)
524+
}
525+
526+
@Test
527+
fun `#getState should return Disposed even if different state was set after disposal`() {
528+
// given
529+
val editorResource = createEditorResource(POD2)
530+
editorResource.disposed.set(true)
531+
val shouldBeIgnored = mock<Error>()
532+
editorResource.setState(shouldBeIgnored)
533+
// when
534+
val state = editorResource.getState()
535+
// then
536+
assertThat(state).isInstanceOf(Disposed::class.java)
537+
}
538+
516539
@Test
517540
fun `#dispose should close clusterResource that was created`() {
518541
// given

src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResourcesTest.kt

+4-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import java.util.*
3333
import org.assertj.core.api.Assertions.assertThat
3434
import org.junit.After
3535
import org.junit.Test
36+
import java.util.concurrent.atomic.AtomicBoolean
3637

3738
class EditorResourcesTest {
3839

@@ -230,8 +231,10 @@ class EditorResourcesTest {
230231
}
231232

232233
private fun createEditorResourceMock(resource: HasMetadata): EditorResource {
234+
val disposed = AtomicBoolean(false)
233235
val editorResource: EditorResource = mock {
234-
on { getResource() } doReturn resource
236+
on { mock.getResource() } doReturn resource
237+
on { mock.disposed } doReturn disposed
235238
}
236239
// store editorResource mock in list
237240
editorResources.add(editorResource)

src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/ResourceWatchTest.kt

+5-1
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,11 @@ class ResourceWatchTest {
222222
class TestableResourceWatch(
223223
watchOperations: BlockingDeque<WatchOperation<*>>,
224224
watchOperationsRunner: Runnable = mock()
225-
): ResourceWatch<ResourceKind<out HasMetadata>>(watchOperations, watchOperationsRunner) {
225+
) : ResourceWatch<ResourceKind<out HasMetadata>>(
226+
watchOperations,
227+
watchOperationsRunner,
228+
{ runnable: Runnable -> runnable.run() }) {
229+
226230
public override val watches = spy(super.watches)
227231

228232
override fun watch(

0 commit comments

Comments
 (0)