작업 실행 옵션(Operation execution options)
작업 실행 옵션(Operation execution options)
Lincheck는 특정 작업이 어떻게 실행될지 제어하는 다양한 옵션을 제공합니다. 예를 들어 단일 스레드에서 작업을 실행하거나, 작업을 한 번만 실행하거나, 작업을 블로킹(blocking)으로 표시하는 등의 옵션이 있습니다.
이 문서에서는 다양한 실행 옵션과 그 설정 방법을 배워 볼게요.
본문
단일 스레드 작업 그룹
일부 작업은 절대 동시에 실행되면 안 됩니다. 대표적인 예로 단일 생산자-단일 소비자(single-producer single-consumer) 큐의 작업들이 있습니다.
절대 병렬로 실행되지 않아야 하는 작업 그룹을 만들려면 작업을 선언할 때 nonParallelGroup 옵션을 사용하세요:
@Operation(nonParallelGroup = "consumers")
fun poll(): Int? = queue.poll()
@Operation(nonParallelGroup = "consumers")
fun peek(): Int? = queue.peek()
@Operation(nonParallelGroup = "producer")
fun offer(x: Int) = queue.offer(x)
@Operation
fun isEmpty(): Boolean = queue.isEmpty()
Lincheck는 non-parallel 그룹에 속한 작업들이 서로 병렬로 실행되지 않도록 보장합니다. 하지만 이 작업들은 non-parallel 그룹 밖의 작업들과는 여전히 병렬로 실행될 수 있습니다:
| --------------------- |
| Thread 1 | Thread 2 |
| --------------------- |
| poll() | offer(0) |
| peek() | offer(0) |
| poll() | isEmpty() |
| poll() | isEmpty() |
| isEmpty() | isEmpty() |
| --------------------- |
단일 사용 작업(Single-use operations)
runOnce 옵션을 사용하면 작업을 테스트 호출당 한 번만 실행할 수 있습니다:
@Operation(runOnce = true)
fun singleOp() = struct.singleOp()
@Operation
fun regularOp() = struct.regularOp()
생성된 시나리오의 예:
| ----------------------------- |
| Thread 1 | Thread 2 |
| ----------------------------- |
| regularOp() | singleOp() |
| regularOp() | regularOp() |
| ----------------------------- |
블로킹 작업(Blocking operations)
작업이 실행을 블로킹하도록 의도되었다면 blocking 옵션을 사용하세요. 테스트가 비차단 보장(non-blocking guarantees)을 확인하는 경우, blocking 옵션으로 표시된 작업에서 실행이 멈추더라도 Lincheck는 테스트를 실패시키지 않습니다:
@Operation(blocking = true)
fun put(key: Int, value: Int) = map.put(key, value)
취소 가능한 작업(Cancelable operations)
작업이 일시 중단될 때 취소될 수 있다면 cancellableOnSuspension 옵션을 사용하세요:
@Operation(cancellableOnSuspension = true)
suspend fun receive() = ch.receive()
다음 채널 테스트를 살펴볼게요:
@Param(name = "value", gen = IntGen::class, conf = "1:3")
class CancellableOnSuspensionTest {
private val ch = Channel<Int>()
@Operation
suspend fun send(@Param(name = "value") value: Int) = ch.send(value)
@Operation(cancellableOnSuspension = true)
suspend fun receive() = ch.receive()
@Test
fun test() = ModelCheckingOptions()
.iterations(50)
.invocationsPerIteration(1000)
// Report the scenarios even if the test has not failed
.logLevel(LoggingLevel.INFO)
.check(this::class)
}
| cancellableOnSuspension = false | cancellableOnSuspension = true |
| receive()가 일시 중단되면 시나리오의 나머지 동안 일시 중단된 상태로 유지됩니다: | Lincheck는 receive()가 일시 중단된 후 취소되는 시나리오를 탐색합니다. 이는 실제 코루틴 코드를 더 잘 모델링합니다: |
즉시 취소(Prompt cancellation)
cancellableOnSuspension을 활성화하고 작업이 즉시 취소를 지원해야 한다면, promptCancellation을 true로 설정할 수도 있습니다:
@Operation(cancellableOnSuspension = true, promptCancellation = true)
suspend fun receive() = ch.receive()
다음 채널 테스트를 살펴볼게요:
@Param(name = "value", gen = IntGen::class, conf = "1:3")
class PromptCancellationTest {
private val ch = Channel<Int>()
@Operation
suspend fun send(@Param(name = "value") value: Int) = ch.send(value)
@Operation(cancellableOnSuspension = true, promptCancellation = true)
suspend fun receive() = ch.receive()
@Test
fun test() = ModelCheckingOptions()
.iterations(50)
.invocationsPerIteration(1000)
// Report the scenarios even if the test has not failed
.logLevel(LoggingLevel.INFO)
.check(this::class)
}
| promptCancellation = false | promptCancellation = true |
| Lincheck는 작업이 실제로 일시 중단된 이후에만 취소를 시도할 수 있습니다. 취소를 시도하기 전에 작업이 다시 시작되면 취소가 실패하고 작업은 정상적으로 완료됩니다: | Lincheck는 작업이 이미 다시 시작되었지만 실행될 기회가 없었더라도 취소할 수 있습니다. 이는 코루틴이 다시 시작된 시점과 실제로 결과를 처리하는 시점 사이에 취소될 수 있는 실제 환경의 동작을 모델링합니다: |