3. 单次上报流程实现:使用协程进行HTTP POST、超时控制、异常捕获

3.1 章节目标与设计原则

单次上报是整个埋点重试机制的基础单元。本章将实现一个使用Kotlin协程的HTTP POST上报函数,包含:

  • 超时控制:防止网络抖动导致长时间阻塞协程调度池。
  • 异常捕获:区分网络异常、服务器错误、数据序列化错误,为后续重试策略提供精确错误类型。
  • 协程安全:确保在Dispatchers.IO上执行网络请求,不阻塞主线程。
核心设计哲学: 单次上报函数应是“幂等且无副作用”的——它的唯一职责是发送一次请求并返回结果状态,绝不自行决定重试或缓存。

3.2 实现流程总览

下图展示了单次上报的完整流程,从协程启动到结果分派。

单次上报协程流程图 启动协程 (Dispatchers.IO) withTimeout(5s) 超时控制 超时 TimeoutCancellationException OkHttp POST 发送请求 处理响应 / 捕获异常 成功 (2xx) 返回 Result.success 返回 Result.failure(cause) 网络异常 IOException / HttpException

3.3 关键代码实现

下面给出完整的单次上报函数,使用OkHttp作为HTTP客户端,Kotlin协程进行异步控制。

suspend fun reportSingleEvent(
    event: Event,
    client: OkHttpClient,
    url: String,
    timeoutMillis: Long = 5000
): Result<Unit> {
    return withContext(Dispatchers.IO) {
        try {
            // 超时控制
            withTimeout(timeoutMillis) {
                val jsonBody = event.toJson().toRequestBody(MediaType.parse("application/json")!!)
                val request = Request.Builder()
                    .url(url)
                    .post(jsonBody)
                    .build()
                val response = client.newCall(request).execute()
                if (!response.isSuccessful) {
                    throw HttpException(response.code, response.message)
                }
                response.close()
                Result.success(Unit)
            }
        } catch (e: TimeoutCancellationException) {
            // 超时异常单独处理
            Result.failure(EventTimeoutException("上报超时: $timeoutMillis ms", e))
        } catch (e: HttpException) {
            Result.failure(EventHttpException("HTTP错误: ${e.code}", e))
        } catch (e: IOException) {
            Result.failure(EventNetworkException("网络异常: ${e.message}", e))
        } catch (e: Exception) {
            Result.failure(EventUnknownException("未知错误: ${e.message}", e))
        }
    }
}

// 自定义异常类示例
class EventTimeoutException(msg: String, cause: Throwable?) : Exception(msg, cause)
class EventHttpException(msg: String, cause: Throwable?) : Exception(msg, cause)
class EventNetworkException(msg: String, cause: Throwable?) : Exception(msg, cause)
class EventUnknownException(msg: String, cause: Throwable?) : Exception(msg, cause)
代码要点:
  • withContext(Dispatchers.IO) 确保网络请求不阻塞主线程。
  • withTimeout() 包裹整个请求,超时后抛出 TimeoutCancellationException。
  • 自定义异常类(如 EventTimeoutException)便于上层重试策略区分错误类型。
  • 返回值使用 Result<Unit>,符合Kotlin函数式风格,不强迫调用者try-catch。

3.4 异常类型与处理策略对照表

异常类型 触发场景 推荐重试策略
EventTimeoutException 请求在指定时间内未收到响应 可重试,建议指数退避
EventHttpException 服务器返回非2xx状态码 4xx一般不重试;5xx可重试
EventNetworkException DNS解析、连接拒绝、Socket异常 可重试,但需限制重试次数
EventUnknownException JSON序列化、协程取消等意外错误 不重试或根据具体上下文决定
重要注意事项:
  • 不要将协程取消(CancellationException)捕获后吞掉,否则会破坏协程协作取消机制。上面的代码保留了 CancellationException 的隐式传播(withTimeout 抛出的是 TimeoutCancellationException 继承自 CancellationException)。
  • OkHttp的 Response 必须调用 close() 或使用 use 块,否则连接不会被回收。
  • 建议对 event.toJson() 单独进行try-catch,以隔离序列化异常。

3.5 超时控制深度分析

协程的超时控制通过 withTimeout 实现,原理是启动一个内部的协程计时器,在指定时间后抛出一个 TimeoutCancellationException。这个异常是 CancellationException 的子类,所以它不会导致协程的父协程被取消,除非父协程捕获并忽略。

在实际埋点上报场景中,超时时间建议设置在 3~10秒 之间,过长会导致协程池线程被长时间占用,过短则可能因网络慢导致正常请求失败。

3.6 异常捕获最佳实践

捕获异常时应遵循“精确先行、兜底在后”的原则:

  1. 先捕获具体子类异常(如 TimeoutCancellationException)。
  2. 再捕获中间层异常(如 HttpException、IOException)。
  3. 最后用 Exception 捕获所有未被覆盖的异常。
  4. 捕获后均包装为自定义异常并返回 Result.failure,保留原始 cause 以便调试。
// 精确捕获示例(已被整合到上方函数中)
try {
    // ... 请求逻辑
} catch (e: TimeoutCancellationException) {
    // 超时
} catch (e: HttpException) {
    // HTTP错误
} catch (e: IOException) {
    // 网络错误
} catch (e: Exception) {
    // 兜底
}
调试技巧: 在开发阶段,可以开启 OkHttp 的日志拦截器来查看实际网络请求和响应细节。但在生产环境中应关闭或使用 DEBUG 级别避免敏感信息泄露。

3.7 单元测试验证

测试单次上报函数的核心是验证超时和异常分支的正确性。使用 MockWebServer(OkHttp内置)可以模拟不同场景:

// 模拟超时场景
val mockServer = MockWebServer()
mockServer.enqueue(MockResponse().setBodyDelay(10, TimeUnit.SECONDS)) // 延迟10秒
mockServer.start()

val client = OkHttpClient.Builder()
    .connectTimeout(1, TimeUnit.SECONDS)
    .readTimeout(1, TimeUnit.SECONDS)
    .build()

val result = reportSingleEvent(
    event = testEvent,
    client = client,
    url = mockServer.url("/").toString(),
    timeoutMillis = 2000
)

assertTrue(result.isFailure)
assertTrue(result.exceptionOrNull() is EventTimeoutException)

mockServer.shutdown()

通过上述实现,单次上报函数具备了超时控制、精确异常分类和协程安全三大特性,为后续的重试机制提供了可靠的基础单元。