{"id":35853954,"url":"https://github.com/santimattius/kmp-resilient","last_synced_at":"2026-06-07T00:00:36.477Z","repository":{"id":328519819,"uuid":"1110256710","full_name":"santimattius/kmp-resilient","owner":"santimattius","description":"A Kotlin Multiplatform library providing resilience patterns (Timeout, Retry, Circuit Breaker, Rate Limiter, Bulkhead, Hedging, Cache, Fallback) for suspend functions. Compose them declaratively with a small DSL and observe runtime telemetry via Flow.","archived":false,"fork":false,"pushed_at":"2026-06-03T23:59:28.000Z","size":431,"stargazers_count":136,"open_issues_count":5,"forks_count":5,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-04T02:12:51.245Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/santimattius.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-12-04T23:46:46.000Z","updated_at":"2026-06-01T20:25:37.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/santimattius/kmp-resilient","commit_stats":null,"previous_names":["santimattius/kmp-resilient"],"tags_count":13,"template":false,"template_full_name":"santimattius/kmp-basic-template","purl":"pkg:github/santimattius/kmp-resilient","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/santimattius%2Fkmp-resilient","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/santimattius%2Fkmp-resilient/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/santimattius%2Fkmp-resilient/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/santimattius%2Fkmp-resilient/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/santimattius","download_url":"https://codeload.github.com/santimattius/kmp-resilient/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/santimattius%2Fkmp-resilient/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34003814,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-06T02:00:07.033Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-01-08T08:54:51.994Z","updated_at":"2026-06-07T00:00:36.460Z","avatar_url":"https://github.com/santimattius.png","language":"Kotlin","funding_links":[],"categories":["容错组件","Libraries"],"sub_categories":["Spring Cloud框架","➿ Asynchronous"],"readme":"# Resilient  - Kotlin Multiplatform Library\n\nA Kotlin Multiplatform library providing resilience patterns (Timeout, Retry, Circuit Breaker, Rate Limiter, Bulkhead, Hedging, Cache, Fallback) for suspend functions. Compose them declaratively with a small DSL and observe runtime telemetry via Flow.\n\n## Quick Start\n```kotlin\nimport com.santimattius.resilient.composition.resilient\nimport com.santimattius.resilient.retry.ExponentialBackoff\nimport kotlin.time.Duration.Companion.seconds\nimport kotlin.time.Duration.Companion.milliseconds\n\nval policy = resilient {\n    timeout { timeout = 2.seconds }\n    retry {\n        maxAttempts = 3\n        backoffStrategy = ExponentialBackoff(initialDelay = 100.milliseconds)\n    }\n    circuitBreaker {\n        failureThreshold = 5\n        successThreshold = 2\n        timeout = 30.seconds\n    }\n}\n\nsuspend fun call(): String = policy.execute { fetchData() }\n```\n\n## Telemetry\nObserve runtime events from `policy.events` (SharedFlow):\n- **RetryAttempt**(attempt, error)\n- **CircuitStateChanged**(from, to)\n- **RateLimited**(waitTime)\n- **BulkheadRejected**(reason)\n- **CacheHit**(key) / **CacheMiss**(key)\n- **TimeoutTriggered**(timeout)\n- **FallbackTriggered**(error)\n- **HedgingUsed**(attemptIndex)\n- **OperationSuccess**(duration) / **OperationFailure**(error, duration)\n\n```kotlin\nval job = scope.launch {\n    policy.events.collect { event -\u003e println(event) }\n}\n```\n\n## Composition Order\nOuter → Inner (default):\n- Fallback → Cache → Coalesce → Timeout → Retry → Circuit Breaker → Rate Limiter → Bulkhead → Hedging → Block\n- Fallback wraps outermost to handle failures after all policies.\n\n### Custom Composition Order\nThe composition order is configurable. Use `compositionOrder()` to specify a custom order:\n\n```kotlin\nimport com.santimattius.resilient.composition.OrderablePolicyType\n\nval policy = resilient(scope) {\n    compositionOrder(listOf(\n        OrderablePolicyType.CACHE,        // Check cache first (after Fallback)\n        OrderablePolicyType.COALESCE,     // Deduplicate in-flight requests by key\n        OrderablePolicyType.TIMEOUT,      // Then apply timeout\n        OrderablePolicyType.RETRY,        // Retry on failures\n        OrderablePolicyType.CIRCUIT_BREAKER,\n        OrderablePolicyType.RATE_LIMITER,\n        OrderablePolicyType.BULKHEAD,\n        OrderablePolicyType.HEDGING\n    ))\n    // Fallback is automatically added as the outermost policy\n    // ... configure policies\n}\n```\n\n**Important Notes:**\n- **Fallback is not included** in `OrderablePolicyType` - it is always positioned outermost automatically\n- `compositionOrder(...)` accepts a subset: policy types not included are appended in the default order\n- Duplicates are removed\n- Fallback must remain outermost to catch all failures from other policies\n- The order affects how policies interact with each other, so choose carefully based on your use case\n\n### Timeout vs Retry order\n- **Timeout outer, Retry inner** (default): The timeout applies to the **entire** execution (all retries combined). A single slow attempt can consume the full timeout; if it expires, the whole operation fails.\n- **Retry outer, Timeout inner**: Each **attempt** has its own timeout. You can get more attempts within the same total wall-clock time. Use `compositionOrder(listOf(..., OrderablePolicyType.RETRY, OrderablePolicyType.TIMEOUT, ...))` if you want per-attempt timeout.\n\n---\n\n## Features\n\n### Timeout\nAborts the operation after a configured duration. `onTimeout` runs only on actual timeouts. This is a **single** time limit for the block as wrapped by the policy (see [Timeout vs Retry order](#timeout-vs-retry-order)). For a timeout **per retry attempt**, use [Retry](#retry) with `perAttemptTimeout` instead.\n```kotlin\nval policy = resilient {\n    timeout {\n        timeout = 3.seconds\n        onTimeout = { /* log metric */ }\n    }\n}\n```\n→ [In-depth documentation](docs/patterns/timeout.md)\n\n### Retry\nRetries failing operations according to a backoff strategy and predicate. Use **shouldRetry** to avoid retrying non-transient errors (e.g. 4xx client errors); retry only on 5xx, network/IO, or timeouts. Optional **perAttemptTimeout** limits each attempt (including the first) so a single slow attempt does not consume the whole retry budget.\n\nOptional **shouldRetryResult** retries when the block returns successfully but the value is not acceptable (e.g. HTTP 202, empty body, “not ready” flag). Return `true` from the predicate to request another attempt; it shares the same **maxAttempts** budget as exception-based retries. **`onRetry`** receives `RetryableResultException` (with `lastValue`) for telemetry; if attempts are exhausted while the predicate stays `true`, the **last returned value** is returned (unlike exception exhaustion, which rethrows).\n```kotlin\nimport com.santimattius.resilient.retry.*\n\nval policy = resilient {\n    retry {\n        maxAttempts = 4\n        shouldRetry = { it is java.io.IOException }  // e.g. only retry IO; do not retry 4xx\n        shouldRetryResult = { status -\u003e (status as Int) \u003e= 500 } // e.g. retry on 5xx-like codes returned as value\n        perAttemptTimeout = 5.seconds               // optional: timeout per attempt\n        backoffStrategy = ExponentialBackoff(\n            initialDelay = 200.milliseconds,\n            maxDelay = 5.seconds,\n            factor = 2.0,\n            jitter = true\n        )\n    }\n}\n```\nSupported backoffs: `ExponentialBackoff`, `LinearBackoff`, `FixedBackoff`, `DecorrelatedJitterBackoff`.\n\n**`DecorrelatedJitterBackoff`** implements the AWS decorrelated jitter formula — each caller's delay sequence is independent of every other caller's, which breaks synchronized retry waves in high-concurrency scenarios:\n```kotlin\nimport com.santimattius.resilient.retry.DecorrelatedJitterBackoff\n\nval policy = resilient {\n    retry {\n        maxAttempts     = 4\n        backoffStrategy = DecorrelatedJitterBackoff(base = 100.milliseconds, cap = 10.seconds)\n    }\n}\n```\n→ [In-depth documentation](docs/patterns/retry.md)\n\n### Circuit Breaker\nPrevents hammering failing downstreams. States: CLOSED → OPEN → HALF_OPEN.\n\nThree trip modes are available:\n\n- **Consecutive** (default): `failureThreshold` consecutive failures open the circuit; a success resets the count.\n- **Sliding window** (`slidingWindow`): `failureThreshold` failures within a time window open the circuit.\n- **Failure-rate** (`failureRateThreshold`): circuit opens when the failure rate over the last `minimumNumberOfCalls` outcomes meets or exceeds a percentage.\n\n```kotlin\nval policy = resilient(scope) {\n    circuitBreaker {\n        failureThreshold = 5\n        successThreshold = 2\n        halfOpenMaxCalls = 1\n        timeout          = 60.seconds\n        // slidingWindow = 30.seconds       // time-based mode (mutually exclusive with failureRateThreshold)\n        // failureRateThreshold = 50.0      // failure-rate mode — opens at 50% over last minimumNumberOfCalls\n        // minimumNumberOfCalls = 10        // ring-buffer size (default 10)\n    }\n}\n```\n\n**`shouldRecordResult`** counts a successful return value as a failure (while still returning it to the caller) — useful when the downstream signals errors inside HTTP 200 responses:\n```kotlin\nval policy = resilient(scope) {\n    circuitBreaker {\n        failureThreshold   = 3\n        shouldRecordResult = { result -\u003e result is ApiResponse \u0026\u0026 result.code == 503 }\n    }\n}\n```\n\n**Named / shared circuit breaker:** use `CircuitBreakerRegistry` so multiple policies share the same breaker state (e.g. all `\"payments\"` calls trip a single breaker). Cannot combine `circuitBreaker { }` and `circuitBreakerNamed(...)` in the same policy.\n```kotlin\nimport com.santimattius.resilient.circuitbreaker.CircuitBreakerRegistry\n\nval circuitBreakers = CircuitBreakerRegistry()\n\nval policyA = resilient(scope) {\n    circuitBreakerNamed(circuitBreakers, \"payments\") {\n        failureThreshold = 5\n        timeout          = 30.seconds\n    }\n}\nval policyB = resilient(scope) {\n    circuitBreakerNamed(circuitBreakers, \"payments\") { } // same instance as policyA\n}\n```\n→ [In-depth documentation](docs/patterns/circuit-breaker.md)\n\n### Rate Limiter\nToken-bucket rate limiting: tokens refill at the **start of each period** (fixed-window refill). With `maxCalls = 10` and `period = 1.seconds`, at most 10 calls per second are allowed. One bucket per policy (global limit). Optional max wait when limited.\n```kotlin\nval policy = resilient {\n    rateLimiter {\n        maxCalls = 10\n        period = 1.seconds\n        timeoutWhenLimited = 5.seconds // throw if wait would exceed\n        onRateLimited = { /* metric */ }\n    }\n}\n```\n**Named / shared rate limiter:** use `RateLimiterRegistry` so multiple policies share the same token-bucket quota (e.g. all `\"payments\"` calls consume from the same pool). Cannot combine `rateLimiter { }` and `rateLimiterNamed(...)` in the same policy.\n```kotlin\nimport com.santimattius.resilient.ratelimiter.RateLimiterRegistry\n\nval rateLimiters = RateLimiterRegistry()\n\nval policyA = resilient(scope) {\n    rateLimiterNamed(rateLimiters, \"payments\") { maxCalls = 10; period = 1.seconds }\n}\nval policyB = resilient(scope) {\n    rateLimiterNamed(rateLimiters, \"payments\") { } // same instance as policyA\n}\n```\n→ [In-depth documentation](docs/patterns/rate-limiter.md)\n\n### Bulkhead\nLimit concurrent executions and queued waiters; optional acquire timeout.\n```kotlin\nval policy = resilient(scope) {\n    bulkhead {\n        maxConcurrentCalls = 8\n        maxWaitingCalls = 32\n        timeout = 2.seconds\n    }\n}\n```\n→ [In-depth documentation](docs/patterns/bulkhead.md)\n\n**Named / shared bulkhead:** use `BulkheadRegistry` so several policies share the same pool (e.g. one limit for all `\"database\"` calls). Cannot combine `bulkhead { }` and `bulkheadNamed(...)` in the same policy.\n```kotlin\nimport com.santimattius.resilient.bulkhead.BulkheadRegistry\n\nval bulkheads = BulkheadRegistry()\n\nval policyA = resilient(scope) {\n    bulkheadNamed(bulkheads, \"database\") {\n        maxConcurrentCalls = 4\n        maxWaitingCalls = 16\n    }\n}\nval policyB = resilient(scope) {\n    bulkheadNamed(bulkheads, \"database\") {\n        maxConcurrentCalls = 4 // first registration wins; same instance as policyA\n    }\n}\n```\n\n### Hedging\nLaunch parallel attempts and return the first successful result. Use carefully (extra load).\n```kotlin\nval policy = resilient {\n    hedging {\n        attempts = 3\n        stagger = 50.milliseconds\n    }\n}\n```\n→ [In-depth documentation](docs/patterns/hedging.md)\n\n### Cache (In-Memory TTL)\nCache successful results per key with TTL. Use a fixed **key** or a **keyProvider** (suspend) for dynamic keys (e.g. per user, per request). Invalidation is available via `policy.cacheHandle` when cache is configured.\n```kotlin\nval policy = resilient(scope) {\n    cache {\n        key = \"users:123\"                    // fixed key\n        // or keyProvider = { \"user:${userId}\" }  // dynamic key\n        ttl = 30.seconds\n    }\n}\n\n// Invalidate by key or prefix (e.g. after write)\npolicy.cacheHandle?.invalidate(\"users:123\")\npolicy.cacheHandle?.invalidatePrefix(\"user:\")\n```\nThe in-memory implementation is one possible `CachePolicy`; custom backends (e.g. persistent storage or Redis) can implement the same interface.\n→ [In-depth documentation](docs/patterns/cache.md)\n\n### Coalescing (Request Deduplication)\nDeduplicates **concurrent in-flight** executions by key. If multiple callers resolve the same key while the operation is still running, only one block execution happens and all callers share the same result (or error). It does **not** cache completed results; for TTL caching, use `cache { ... }`.\n```kotlin\nval policy = resilient(scope) {\n    coalesce {\n        key = \"profile:42\" // fixed key\n        // or keyProvider = { \"profile:${userId}\" } // dynamic key\n    }\n}\n```\n\n### CoroutineScope Binding\n\nBy default you pass a `ResilientScope` explicitly. When working inside a framework that already manages a `CoroutineScope` (e.g. `viewModelScope`, `lifecycleScope`, `rememberCoroutineScope`), you can bind the policy lifecycle directly to that scope — no manual `ResilientScope` creation or `policy.close()` needed.\n\n**`CoroutineScope.asResilientScope()`** — wraps an existing scope as a `ResilientScope`. Cancelling the outer scope automatically cancels all internal background jobs (cache cleanup, coalescing deduplication):\n\n```kotlin\nimport com.santimattius.resilient.composition.asResilientScope\n\n// Android ViewModel\nclass ProfileViewModel : ViewModel() {\n    private val scope  = viewModelScope.asResilientScope()\n    private val policy = resilient(scope) {\n        cache { key = \"profile\"; ttl = 60.seconds }\n        retry { maxAttempts = 3 }\n    }\n    // When the ViewModel is cleared, viewModelScope is cancelled → scope and all policy\n    // background jobs are cancelled automatically. No policy.close() needed.\n}\n```\n\n**`CoroutineScope.resilient { }`** — shorthand that combines `.asResilientScope()` and `resilient(scope) { }` in one call:\n\n```kotlin\nimport com.santimattius.resilient.composition.resilient\n\nclass ProfileViewModel : ViewModel() {\n    private val policy = viewModelScope.resilient {\n        cache { key = \"profile\"; ttl = 60.seconds }\n        retry { maxAttempts = 3 }\n    }\n}\n```\n\n**Lifecycle contract:**\n- When the outer `CoroutineScope` is cancelled → the derived scope and all its background jobs are cancelled automatically via structured concurrency.\n- Calling `policy.close()` stops only the internal policy jobs — it does **not** cancel the outer scope.\n- Do not call these on an already-cancelled scope; internal coroutine launches will fail with `CancellationException`.\n\n### Health / Readiness\nUse `policy.getHealthSnapshot()` to build health or readiness endpoints (e.g. Kubernetes probes, `/health` API). The snapshot includes circuit breaker state, bulkhead usage, rate limiter quota, retry config, and cache stats when the corresponding policies are configured.\n```kotlin\nimport com.santimattius.resilient.circuitbreaker.CircuitState\n\nval snapshot = policy.getHealthSnapshot()\n\n// Circuit breaker state\nval healthy = snapshot.circuitBreaker?.state != CircuitState.OPEN\n// snapshot.circuitBreaker?.failureCount, successCount\n\n// Bulkhead: active and waiting counts\nsnapshot.bulkhead?.let { bh -\u003e\n    // bh.activeConcurrentCalls, bh.waitingCalls, bh.maxConcurrentCalls, bh.maxWaitingCalls\n}\n\n// Rate limiter: remaining tokens and time until next refill\nsnapshot.rateLimiter?.let { rl -\u003e\n    // rl.remainingCalls, rl.timeToRefill\n}\n\n// Retry: configured max attempts\nsnapshot.retry?.let { r -\u003e\n    // r.maxAttempts\n}\n\n// Cache: entry count and cumulative hit rate (Double.NaN when no calls yet)\nsnapshot.cache?.let { c -\u003e\n    // c.entryCount, c.hitRate\n}\n```\n\n### Fallback\nReturn a fallback value when the operation fails. **Ensure the fallback return type matches the type returned by the block** passed to `execute()`, otherwise a `ClassCastException` may occur at runtime.\n```kotlin\nimport com.santimattius.resilient.fallback.FallbackConfig\n\nval policy = resilient {\n    fallback(FallbackConfig { err -\u003e\n        // produce a fallback from error (log/metric here)\n        \"fallback-value\"\n    })\n}\n```\n→ [In-depth documentation](docs/patterns/fallback.md)\n\n---\n\n## Combined Example\n```kotlin\nval policy = resilient {\n    cache { key = \"profile:42\"; ttl = 20.seconds }\n    timeout { timeout = 2.seconds }\n    retry {\n        maxAttempts = 3\n        backoffStrategy = ExponentialBackoff(initialDelay = 100.milliseconds)\n    }\n    circuitBreaker { failureThreshold = 5; successThreshold = 2; halfOpenMaxCalls = 1 }\n    rateLimiter { maxCalls = 50; period = 1.seconds }\n    bulkhead { maxConcurrentCalls = 10; maxWaitingCalls = 100 }\n    hedging { attempts = 2 }\n    fallback(FallbackConfig { \"cached-or-default\" })\n}\n\nsuspend fun load(): User = policy.execute { loadProfile() }\n```\n\n## Android (Compose) Example\nA ready-to-run sample is available at:\n- `androidApp/src/main/kotlin/com/santimattius/kmp/ResilientExample.kt`\n\nUse it from `MainActivity`:\n```kotlin\nsetContent { ResilientExample() }\n```\n\n## Best Practices\n- Start simple: Timeout + Retry. Add Circuit Breaker for flaky dependencies.\n- **shouldRetry:** Do not retry on 4xx client errors (e.g. 400, 404, 409); retry only on transient failures (5xx, network/IO, timeouts). This avoids amplifying bad requests and keeps idempotency expectations clear.\n- Rate limiter for external APIs; Bulkhead for database or threadpool protection.\n- Hedging reduces tail latency but raises load—use selectively.\n- Cache only idempotent reads; set TTL appropriately.\n- Always observe telemetry (including CacheHit/CacheMiss, FallbackTriggered) for visibility and tuning.\n\n## Testing with `resilient-test`\n\nThe `resilient-test` module provides utilities for testing resilience policies with fault injection and simplified policy builders.\n\n### Fault Injection\n\nUse `FaultInjector` to simulate failures, delays, and intermittent behavior in your tests. Prefer **`failCount`** for unit tests — it produces deterministic results and avoids intermittent failures.\n\n```kotlin\nimport com.santimattius.resilient.test.FaultInjector\nimport kotlin.time.Duration.Companion.milliseconds\n\n// Deterministic (preferred for unit tests): fail the first N calls, then succeed\nval injector = FaultInjector.builder()\n    .failCount(3)   // fails calls 1–3, succeeds on call 4\n    .build()\n\n// Probabilistic: use only for chaos / load tests, not unit tests\nval chaosInjector = FaultInjector.builder()\n    .failureRate(0.3)           // 30% chance of failure per call\n    .delay(50.milliseconds)     // Add 50ms delay\n    .delayJitter(true)          // Randomize delay ±20%\n    .exception { CustomException() }\n    .build()\n```\n\n**Configuration:**\n- `failCount(n: Int)`: Fail the first `n` calls deterministically, then always succeed. Takes precedence over `failureRate`. **Preferred for unit tests.**\n- `failureRate(rate: Double)`: Probability of throwing an exception per call (0.0 = never, 1.0 = always). Ignored when `failCount \u003e 0`.\n- `exception(block: () -\u003e Throwable)`: Factory for the exception to throw (default: `FaultInjectedException`).\n- `delay(duration: Duration)`: Fixed delay before executing the block (default: `Duration.ZERO`).\n- `delayJitter(enable: Boolean)`: Randomize delay ±20% (default: `false`).\n\n### Policy Builders for Tests\n\nUse `PolicyBuilders` to create policies with sensible test defaults:\n\n```kotlin\nimport com.santimattius.resilient.test.PolicyBuilders\nimport com.santimattius.resilient.test.TestResilientScope\nimport kotlinx.coroutines.test.runTest\n\n@Test\nfun `test retry with fault injection`() = runTest {\n    val scope = TestResilientScope()\n    val policy = PolicyBuilders.retryPolicy(\n        scope,\n        maxAttempts = 3,\n        initialDelay = 10.milliseconds\n    )\n\n    val injector = FaultInjector.builder()\n        .failCount(2)  // fail first 2 calls, succeed on the 3rd\n        .build()\n\n    val result = policy.execute {\n        injector.execute { \"success\" }\n    }\n\n    assertEquals(\"success\", result)\n}\n```\n\n**Available builders:**\n- `retryPolicy(scope, maxAttempts = 3, initialDelay = 10ms, maxDelay = 100ms, shouldRetry = { true })`\n- `timeoutPolicy(scope, timeout = 1.second)`\n- `circuitBreakerPolicy(scope, failureThreshold = 3, successThreshold = 2, timeout = 5.seconds)`\n- `bulkheadPolicy(scope, maxConcurrentCalls = 2, maxWaitingCalls = 4)`\n- `rateLimiterPolicy(scope, maxCalls = 5, period = 1.second)`\n\n### TestResilientScope\n\nUse `TestResilientScope` to create a test-friendly scope for policies:\n\n```kotlin\nimport com.santimattius.resilient.test.TestResilientScope\nimport kotlinx.coroutines.test.runTest\n\n@Test\nfun `test policy lifecycle`() = runTest {\n    val scope = TestResilientScope(this)\n    val policy = resilient(scope) {\n        retry { maxAttempts = 3 }\n    }\n    \n    // ... test logic\n    \n    scope.cancel() // cleanup\n}\n```\n\n### Gradle Dependency\n\nAdd the `resilient-test` module to your test dependencies:\n\n```kotlin\n// build.gradle.kts\nkotlin {\n    sourceSets {\n        commonTest.dependencies {\n            implementation(\"io.github.santimattius.resilient:resilient-test:1.5.0\")\n            implementation(\"org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0\")\n        }\n    }\n}\n```\n\n## Testing Notes\n- Use `kotlinx-coroutines-test` for virtual time with `runTest` and `TestTimeSource`.\n- Use `FaultInjector` to simulate realistic failure scenarios (intermittent errors, slow responses).\n- Verify: retry attempts and delays, CB transitions, RL windows, BH limits, composition order, cancellation propagation, telemetry.\n\n---\n\n## Pattern Reference\n\nTheoretical and technical deep-dive documentation for each pattern — definition, when to apply, configuration reference, and extended examples:\n\n| Pattern | Document |\n|---|---|\n| Timeout | [docs/patterns/timeout.md](docs/patterns/timeout.md) |\n| Retry | [docs/patterns/retry.md](docs/patterns/retry.md) |\n| Circuit Breaker | [docs/patterns/circuit-breaker.md](docs/patterns/circuit-breaker.md) |\n| Rate Limiter | [docs/patterns/rate-limiter.md](docs/patterns/rate-limiter.md) |\n| Bulkhead | [docs/patterns/bulkhead.md](docs/patterns/bulkhead.md) |\n| Hedging | [docs/patterns/hedging.md](docs/patterns/hedging.md) |\n| Cache | [docs/patterns/cache.md](docs/patterns/cache.md) |\n| Fallback | [docs/patterns/fallback.md](docs/patterns/fallback.md) |\n\n→ [Full index with descriptions](docs/README.md)\n\n---\n\n## Contributing\n\nContributions are welcome. Please read [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, coding guidelines, and the pull request process.\n\n- [Code of Conduct](CODE_OF_CONDUCT.md)\n- [Security policy](SECURITY.md)\n- [Changelog](CHANGELOG.md)\n\nThis project is licensed under the [Apache License 2.0](LICENSE).\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsantimattius%2Fkmp-resilient","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsantimattius%2Fkmp-resilient","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsantimattius%2Fkmp-resilient/lists"}