작업 실행 옵션(Operation execution options)

작업 실행 옵션(Operation execution options)

Lincheck는 특정 작업이 어떻게 실행될지 제어하는 다양한 옵션을 제공합니다. 예를 들어 단일 스레드에서 작업을 실행하거나, 작업을 한 번만 실행하거나, 작업을 블로킹(blocking)으로 표시하는 등의 옵션이 있습니다.

이 문서에서는 다양한 실행 옵션과 그 설정 방법을 배워 볼게요.

출처: Operation execution options

본문

단일 스레드 작업 그룹

일부 작업은 절대 동시에 실행되면 안 됩니다. 대표적인 예로 단일 생산자-단일 소비자(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을 활성화하고 작업이 즉시 취소를 지원해야 한다면, promptCancellationtrue로 설정할 수도 있습니다:

    @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는 작업이 이미 다시 시작되었지만 실행될 기회가 없었더라도 취소할 수 있습니다. 이는 코루틴이 다시 시작된 시점과 실제로 결과를 처리하는 시점 사이에 취소될 수 있는 실제 환경의 동작을 모델링합니다: |

더 알아보기