• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright 2017-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3  */
4 
5 @file:Suppress("RedundantVisibilityModifier")
6 
7 package kotlinx.atomicfu
8 
9 import java.util.*
10 import java.util.concurrent.atomic.*
11 import java.util.concurrent.locks.*
12 import kotlin.coroutines.*
13 import kotlin.coroutines.intrinsics.*
14 
15 private const val PAUSE_EVERY_N_STEPS = 1000
16 private const val STALL_LIMIT_MS = 15_000L // 15s
17 private const val SHUTDOWN_CHECK_MS = 10L // 10ms
18 
19 private const val STATUS_DONE = Int.MAX_VALUE
20 
21 private const val MAX_PARK_NANOS = 1_000_000L // part for at most 1ms just in case of loosing unpark signal
22 
23 /**
24  * Environment for performing lock-freedom tests for lock-free data structures
25  * that are written with [atomic] variables.
26  */
27 public open class LockFreedomTestEnvironment(
28     private val name: String,
29     private val allowSuspendedThreads: Int = 0
30 ) {
31     private val interceptor = Interceptor()
32     private val threads = mutableListOf<TestThread>()
33     private val performedOps = LongAdder()
34     private val uncaughtException = AtomicReference<Throwable?>()
35     private var started = false
36     private var performedResumes = 0
37 
38     @Volatile
39     private var completed = false
40     private val onCompletion = mutableListOf<() -> Unit>()
41 
42     private val ueh = Thread.UncaughtExceptionHandler { t, e ->
43         synchronized(System.out) {
44             println("Uncaught exception in thread $t")
45             e.printStackTrace(System.out)
46             uncaughtException.compareAndSet(null, e)
47         }
48     }
49 
50     // status < 0             - inv paused thread id
51     // status >= 0            - no. of performed resumes so far (==last epoch)
52     // status == STATUS_DONE - done working
53     private val status = AtomicInteger()
54     private val globalPauseProgress = AtomicInteger()
55     private val suspendedThreads = ArrayList<TestThread>()
56 
57     @Volatile
58     private var isActive = true
59 
60     // ---------- API ----------
61 
62     /**
63      * Starts lock-freedom test for a given duration in seconds,
64      * invoking [progress] every second (it will be invoked `seconds + 1` times).
65      */
66     public fun performTest(seconds: Int, progress: () -> Unit = {}) {
67         check(isActive) { "Can perform test at most once on this instance" }
68         println("=== $name")
69         val minThreads = 2 + allowSuspendedThreads
70         check(threads.size >= minThreads) { "Must define at least $minThreads test threads" }
71         lockAndSetInterceptor(interceptor)
72         started = true
73         var nextTime = System.currentTimeMillis()
74         threads.forEach { thread ->
75             thread.setUncaughtExceptionHandler(ueh)
76             thread.lastOpTime = nextTime
77             thread.start()
78         }
79         try {
80             var second = 0
81             while (uncaughtException.get() == null) {
82                 waitUntil(nextTime)
83                 println("--- $second: Performed ${performedOps.sum()} operations${resumeStr()}")
84                 progress()
85                 checkStalled()
86                 if (++second > seconds) break
87                 nextTime += 1000L
88             }
89         } finally {
90             complete()
91         }
92         println("------ Done with ${performedOps.sum()} operations${resumeStr()}")
93         progress()
94     }
95 
96     private fun complete() {
97         val activeNonPausedThreads: MutableMap<TestThread, Array<StackTraceElement>> = mutableMapOf()
98         val shutdownDeadline = System.currentTimeMillis() + STALL_LIMIT_MS
99         try {
100             completed = true
101             // perform custom completion blocks. For testing of things like channels, these custom completion
102             // blocks close all the channels, so that all suspended coroutines shall get resumed.
103             onCompletion.forEach { it() }
104             // signal shutdown to all threads (non-paused threads will terminate)
105             isActive = false
106             // wait for threads to terminate
107             while (System.currentTimeMillis() < shutdownDeadline) {
108                 // Check all threads while shutting down:
109                 // All terminated threads are considered to make progress for the purpose of resuming stalled ones
110                 activeNonPausedThreads.clear()
111                 for (t in threads) {
112                     when {
113                         !t.isAlive -> t.makeProgress(getPausedEpoch()) // not alive - makes progress
114                         t.index.inv() == status.get() -> {} // active, paused -- skip
115                         else -> {
116                             val stackTrace = t.stackTrace
117                             if (t.isAlive) activeNonPausedThreads[t] = stackTrace
118                         }
119                     }
120                 }
121                 if (activeNonPausedThreads.isEmpty()) break
122                 checkStalled()
123                 Thread.sleep(SHUTDOWN_CHECK_MS)
124             }
125             activeNonPausedThreads.forEach { (t, stackTrack) ->
126                 println("=== $t had failed to shutdown in time")
127                 stackTrack.forEach { println("\tat $it") }
128             }
129         } finally {
130             shutdown(shutdownDeadline)
131         }
132         // if no other exception was throws & we had threads that did not shut down -- still fails
133         if (activeNonPausedThreads.isNotEmpty()) error("Some threads had failed to shutdown in time")
134     }
135 
136     private fun shutdown(shutdownDeadline: Long) {
137         // forcefully unpause paused threads to shut them down (if any left)
138         val curStatus = status.getAndSet(STATUS_DONE)
139         if (curStatus < 0) LockSupport.unpark(threads[curStatus.inv()])
140         threads.forEach {
141             val remaining = shutdownDeadline - System.currentTimeMillis()
142             if (remaining > 0) it.join(remaining)
143         }
144         // abort waiting threads (if still any left)
145         threads.forEach { it.abortWait() }
146         // cleanup & be done
147         unlockAndResetInterceptor(interceptor)
148         uncaughtException.get()?.let { throw it }
149         threads.find { it.isAlive }?.let { dumpThreadsError("A thread is still alive: $it")}
150     }
151 
152     private fun checkStalled() {
153         val stallLimit = System.currentTimeMillis() - STALL_LIMIT_MS
154         val stalled = threads.filter { it.lastOpTime < stallLimit }
155         if (stalled.isNotEmpty()) dumpThreadsError("Progress stalled in threads ${stalled.map { it.name }}")
156     }
157 
158     private fun resumeStr(): String {
159         val resumes = performedResumes
160         return if (resumes == 0) "" else " (pause/resumes $resumes)"
161     }
162 
163     private fun waitUntil(nextTime: Long) {
164         while (true) {
165             val curTime = System.currentTimeMillis()
166             if (curTime >= nextTime) break
167             Thread.sleep(nextTime - curTime)
168         }
169     }
170 
171     private fun dumpThreadsError(message: String) : Nothing {
172         val traces = threads.associate { it to it.stackTrace }
173         println("!!! $message")
174         println("=== Dumping live thread stack traces")
175         for ((thread, trace) in traces) {
176             if (trace.isEmpty()) continue
177             println("Thread \"${thread.name}\" ${thread.state}")
178             for (t in trace) println("\tat ${t.className}.${t.methodName}(${t.fileName}:${t.lineNumber})")
179             println()
180         }
181         println("===")
182         error(message)
183     }
184 
185     /**
186      * Returns true when test was completed.
187      * Sets to true before calling [onCompletion] blocks.
188      */
189     public val isCompleted: Boolean get() = completed
190 
191     /**
192      * Performs a given block of code on test's completion
193      */
194     public fun onCompletion(block: () -> Unit) {
195         onCompletion += block
196     }
197 
198     /**
199      * Creates a new test thread in this environment that is executes a given lock-free [operation]
200      * in a loop while this environment [isActive].
201      */
202     public fun testThread(name: String? = null, operation: suspend TestThread.() -> Unit): TestThread =
203         TestThread(name, operation)
204 
205     /**
206      * Test thread.
207      */
208     @Suppress("LeakingThis")
209     public inner class TestThread internal constructor(
210         name: String?,
211         private val operation: suspend TestThread.() -> Unit
212     ) : Thread(composeThreadName(name)) {
213         internal val index: Int
214 
215         internal @Volatile var lastOpTime = 0L
216         internal @Volatile var pausedEpoch = -1
217 
218         private val random = Random()
219 
220         // thread-local stuff
221         private var operationEpoch = -1
222         private var progressEpoch = -1
223         private var sink = 0
224 
225         init {
226             check(!started)
227             index = threads.size
228             threads += this
229         }
230 
231         public override fun run() {
232             while (isActive) {
233                 callOperation()
234             }
235         }
236 
237         /**
238          * Use it to insert an arbitrary intermission between lock-free operations.
239          */
240         public inline fun <T> intermission(block: () -> T): T {
241             afterLockFreeOperation()
242             return try { block() }
243                 finally { beforeLockFreeOperation() }
244         }
245 
246         @PublishedApi
247         internal fun beforeLockFreeOperation() {
248             operationEpoch = getPausedEpoch()
249         }
250 
251         @PublishedApi
252         internal fun afterLockFreeOperation() {
253             makeProgress(operationEpoch)
254             lastOpTime = System.currentTimeMillis()
255             performedOps.add(1)
256         }
257 
258         internal fun makeProgress(epoch: Int) {
259             if (epoch <= progressEpoch) return
260             progressEpoch = epoch
261             val total = globalPauseProgress.incrementAndGet()
262             if (total >= threads.size - 1) {
263                 check(total == threads.size - 1)
264                 check(globalPauseProgress.compareAndSet(threads.size - 1, 0))
265                 resumeImpl()
266             }
267         }
268 
269         /**
270          * Inserts random spin wait between multiple lock-free operations in [operation].
271          */
272         public fun randomSpinWaitIntermission() {
273             intermission {
274                 if (random.nextInt(100) < 95) return // be quick, no wait 95% of time
275                 do {
276                     val x = random.nextInt(100)
277                     repeat(x) { sink += it }
278                 } while (x >= 90)
279             }
280         }
281 
282         internal fun stepImpl() {
283             if (random.nextInt(PAUSE_EVERY_N_STEPS) == 0) pauseImpl()
284         }
285 
286         internal fun pauseImpl() {
287             while (true) {
288                 val curStatus = status.get()
289                 if (curStatus < 0 || curStatus == STATUS_DONE) return // some other thread paused or done
290                 pausedEpoch = curStatus + 1
291                 val newStatus = index.inv()
292                 if (status.compareAndSet(curStatus, newStatus)) {
293                     while (status.get() == newStatus) LockSupport.parkNanos(MAX_PARK_NANOS) // wait
294                     return
295                 }
296             }
297         }
298 
299         // ----- Lightweight support for suspending operations -----
300 
301         private fun callOperation() {
302             beforeLockFreeOperation()
303             beginRunningOperation()
304             val result = operation.startCoroutineUninterceptedOrReturn(this, completion)
305             when {
306                 result === Unit -> afterLockFreeOperation() // operation completed w/o suspension -- done
307                 result === COROUTINE_SUSPENDED -> waitUntilCompletion() // operation had suspended
308                 else -> error("Unexpected result of operation: $result")
309             }
310             try {
311                 doneRunningOperation()
312             } catch(e: IllegalStateException) {
313                 throw IllegalStateException("${e.message}; original start result=$result", e)
314             }
315         }
316 
317         private var runningOperation = false
318         private var result: Result<Any?>? = null
319         private var continuation: Continuation<Any?>? = null
320 
321         private fun waitUntilCompletion() {
322             try {
323                 while (true) {
324                     afterLockFreeOperation()
325                     val result: Result<Any?> = waitForResult()
326                     val continuation = takeContinuation()
327                     if (continuation == null) { // done
328                         check(result.getOrThrow() === Unit)
329                         return
330                     }
331                     removeSuspended(this)
332                     beforeLockFreeOperation()
333                     continuation.resumeWith(result)
334                 }
335             } finally {
336                 removeSuspended(this)
337             }
338         }
339 
340         private fun beginRunningOperation() {
341             runningOperation = true
342             result = null
343             continuation = null
344         }
345 
346         @Synchronized
347         private fun doneRunningOperation() {
348             check(runningOperation) { "Should be running operation" }
349             check(result == null && continuation == null) {
350                 "Callback invoked with result=$result, continuation=$continuation"
351             }
352             runningOperation = false
353         }
354 
355         @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
356         @Synchronized
357         private fun resumeWith(result: Result<Any?>, continuation: Continuation<Any?>?) {
358             check(runningOperation) { "Should be running operation" }
359             check(this.result == null && this.continuation == null) {
360                 "Resumed again with result=$result, continuation=$continuation, when this: result=${this.result}, continuation=${this.continuation}"
361             }
362             this.result = result
363             this.continuation = continuation
364             (this as Object).notifyAll()
365         }
366 
367         @Suppress("RESULT_CLASS_IN_RETURN_TYPE", "PLATFORM_CLASS_MAPPED_TO_KOTLIN")
368         @Synchronized
369         private fun waitForResult(): Result<Any?> {
370             while (true) {
371                 val result = this.result
372                 if (result != null) return result
373                 val index = addSuspended(this)
374                 if (index < allowSuspendedThreads) {
375                     // This suspension was permitted, so assume progress is happening while it is suspended
376                     makeProgress(getPausedEpoch())
377                 }
378                 (this as Object).wait(10) // at most 10 ms
379             }
380         }
381 
382         @Synchronized
383         private fun takeContinuation(): Continuation<Any?>? =
384             continuation.also {
385                 this.result = null
386                 this.continuation = null
387             }
388 
389         @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
390         @Synchronized
391         fun abortWait() {
392             this.result = Result.failure(IllegalStateException("Aborted at the end of test"))
393             (this as Object).notifyAll()
394         }
395 
396         private val interceptor: CoroutineContext = object : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
397             override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
398                 Continuation<T>(this) {
399                     @Suppress("UNCHECKED_CAST")
400                     resumeWith(it, continuation as Continuation<Any?>)
401                 }
402         }
403 
404         private val completion = Continuation<Unit>(interceptor) {
405             resumeWith(it, null)
406         }
407     }
408 
409     // ---------- Implementation ----------
410 
411     @Synchronized
412     private fun addSuspended(thread: TestThread): Int {
413         val index = suspendedThreads.indexOf(thread)
414         if (index >= 0) return index
415         suspendedThreads.add(thread)
416         return suspendedThreads.size - 1
417     }
418 
419     @Synchronized
420     private fun removeSuspended(thread: TestThread) {
421         suspendedThreads.remove(thread)
422     }
423 
424     private fun getPausedEpoch(): Int {
425         while (true) {
426             val curStatus = status.get()
427             if (curStatus >= 0) return -1 // not paused
428             val thread = threads[curStatus.inv()]
429             val pausedEpoch = thread.pausedEpoch
430             if (curStatus == status.get()) return pausedEpoch
431         }
432     }
433 
434     internal fun step() {
435         val thread = Thread.currentThread() as? TestThread ?: return
436         thread.stepImpl()
437     }
438 
439     private fun resumeImpl() {
440         while (true) {
441             val curStatus = status.get()
442             if (curStatus == STATUS_DONE) return // done
443             check(curStatus < 0)
444             val thread = threads[curStatus.inv()]
445             performedResumes = thread.pausedEpoch
446             if (status.compareAndSet(curStatus, thread.pausedEpoch)) {
447                 LockSupport.unpark(thread)
448                 return
449             }
450         }
451     }
452 
453     private fun composeThreadName(threadName: String?): String {
454         if (threadName != null) return "$name-$threadName"
455         return name + "-${threads.size + 1}"
456     }
457 
458     private inner class Interceptor : AtomicOperationInterceptor() {
459         override fun <T> beforeUpdate(ref: AtomicRef<T>) = step()
460         override fun beforeUpdate(ref: AtomicInt) = step()
461         override fun beforeUpdate(ref: AtomicLong) = step()
462         override fun <T> afterSet(ref: AtomicRef<T>, newValue: T) = step()
463         override fun afterSet(ref: AtomicInt, newValue: Int) = step()
464         override fun afterSet(ref: AtomicLong, newValue: Long) = step()
465         override fun <T> afterRMW(ref: AtomicRef<T>, oldValue: T, newValue: T) = step()
466         override fun afterRMW(ref: AtomicInt, oldValue: Int, newValue: Int) = step()
467         override fun afterRMW(ref: AtomicLong, oldValue: Long, newValue: Long) = step()
468         override fun toString(): String = "LockFreedomTestEnvironment($name)"
469     }
470 }
471 
472 /**
473  * Manual pause for on-going lock-free operation in a specified piece of code.
474  * Use it for targeted debugging of specific places in code. It does nothing
475  * when invoked outside of test thread.
476  *
477  * **Don't use it in production code.**
478  */
pauseLockFreeOpnull479 public fun pauseLockFreeOp() {
480     val thread = Thread.currentThread() as? LockFreedomTestEnvironment.TestThread ?: return
481     thread.pauseImpl()
482 }