如何在HTTP处理程序中运行x秒

时间:2018-11-01 18:57:36

标签: go

我想将InsertRecords函数运行30秒钟,并测试在给定时间内可以插入多少条记录。

如何在x秒后停止处理InsertRecords,然后从处理程序返回结果?

func benchmarkHandler(w http.ResponseWriter, r *http.Request) {

    counter := InsertRecords()

    w.WriteHeader(200)
    io.WriteString(w, fmt.Sprintf("counter is %d", counter))    
}

func InsertRecords() int {
  counter := 0
  // db code goes here
  return counter
}

2 个答案:

答案 0 :(得分:2)

取消和超时通常是通过context.Context完成的。

虽然这个简单的示例可以单独使用一个通道来完成,但是使用此处的上下文使其更加灵活,并且还可以考虑客户端断开连接的情况。

func benchmarkHandler(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
    defer cancel()

    counter := InsertRecords(ctx)

    w.WriteHeader(200)
    io.WriteString(w, fmt.Sprintf("counter is %d", counter))
}

func InsertRecords(ctx context.Context) int {
    counter := 0
    done := ctx.Done()
    for {
        select {
        case <-done:
            return counter
        default:
        }

        // db code goes here
        counter++
    }
    return counter
}

这将至少运行30秒,并返回完整的数据库迭代次数。如果您想确保处理程序总是在30秒后立即返回,即使阻止了DB调用,那么您也需要将DB代码推入另一个goroutine中,然后让它稍后返回。最短的示例将使用与上述类似的模式,但是同步对计数器变量的访问,因为它可以由DB循环在返回时写入。

func InsertRecords(ctx context.Context) int {
    counter := int64(0)
    done := ctx.Done()

    go func() {
        for {
            select {
            case <-done:
                return
            default:
            }

            // db code goes here
            atomic.AddInt64(&counter, 1)
        }
    }()

    <-done
    return int(atomic.LoadInt64(&counter))
}

有关生产者和超时的示例,请参见@JoshuaKolden的答案,也可以将其与现有请求上下文结合使用。

答案 1 :(得分:2)

JimB指出取消限制HTTP请求所花费的时间可以使用context.WithTimeout进行处理,但是由于您出于基准测试的目的,您可能希望使用更直接的方法。

context.Context的目的是允许发生许多取消事件,并具有正常停止所有下游任务的相同净效果。在JimB的示例中,可能有一些其他过程将在30秒过去之前取消上下文,这从资源利用率的角度来看是理想的。例如,如果连接过早终止,则在构建响应方面没有任何其他工作要做。

如果基准测试是您的目标,那么您希望最大程度地减少多余事件对基准代码的影响。这是如何执行此操作的示例:

com.sun.xml.internal.bind.characterEscapeHandler

本质上,我们正在做的是在离散的db操作上创建无限循环,并计算循环的迭代次数,我们在触发func InsertRecords() int { stop := make(chan struct{}) defer close(stop) countChan := make(chan int) go func() { defer close(countChan) for { // db code goes here select { case countChan <- 1: case <-stop: return } } }() var counter int timeoutCh := time.After(30 * time.Second) for { select { case n := <-countChan: counter += n case <-timeoutCh: return counter } } } 时停止。

JimB示例中的一个问题是,尽管在循环中检查了ctx.Done(),但是如果“ db code”被阻止,循环仍然会被阻止。这是因为ctx.Done()仅与“ db code”块内联评估。

为避免此问题,我们将计时功能和基准测试循环分开,以便没有什么可以阻止我们在发生超时事件时接收它。一旦超时甚至发生,我们将立即返回计数器的结果。 “ db代码”可能仍处于执行中,但是InsertRecords仍将退出并返回其结果。

如果在InsertRecords退出时“ db代码”处于执行中,那么goroutine将保持运行状态,因此我们需要time.After进行清理,以便在函数退出时一定要向goroutine发出信号在下一次迭代中退出。当goroutine退出时,它将清理用于发送计数的通道。

作为一般模式,上面是一个示例,说明了如何在Go中获得精确的计时,而无需考虑代码的实际执行时间。

旁注:更高级的观察结果是,我的示例未尝试在计时器和goroutine之间同步开始时间。在这里解决该问题似乎有些古怪。但是,您可以通过创建一个阻塞主线程的通道来轻松同步两个线程,直到goroutine在开始循环之前将其关闭为止。