No semáforo o limite era de três tarefas ao mesmo tempo.
Mas tem API que não liga para quantas chamadas estão abertas, e sim para quantas você faz por segundo. Passou de dez, toma um 429 e fica de castigo.
Aqui o limite não é de ocupação, é de ritmo.
O que é um sistema de ticket Link para o cabeçalho
Um sistema de ticket controla quando um trabalho pode começar. Uma gorrotina faz o papel de bilheteria e emite tickets a uma taxa fixa, e o trabalhador só executa depois de pegar um.
Ele também aparece como rate limiting ou throttling, mas são nomes aproximados. Já volto nisso.
O trabalhador recebe dois canais, um de trabalhos e um de tickets:
type ticket int
func trabalhador(tickets <-chan ticket, trabalhos <-chan func()) {
for {
// lê o trabalho primeiro, para não gastar ticket à toa
trabalho, ok := <-trabalhos
if !ok {
return
}
<-tickets // espera autorização antes de executar
trabalho()
}
}
Repare na ordem. Se o canal de trabalhos fechou, o trabalhador encerra sem pegar um ticket que ninguém vai usar.
E a bilheteria emite um ticket a cada janela/nTickets:
func bilheteria(ctx context.Context, tickets chan<- ticket, janela time.Duration, nTickets int) {
intervalo := janela / time.Duration(nTickets)
pausa := time.NewTimer(intervalo)
defer pausa.Stop()
for i := 0; ; i++ {
select {
case tickets <- ticket(i):
case <-ctx.Done():
return
}
// o intervalo conta a partir da entrega
pausa.Reset(intervalo)
select {
case <-pausa.C:
case <-ctx.Done():
return
}
}
}
A main pede dez tickets por segundo e manda 31 trabalhos:
go bilheteria(ctx, tickets, 1*time.Second, 10)
go func() {
trabalhador(tickets, trabalhos)
close(pronto)
}()
for i := 0; i <= 30; i++ {
trabalhos <- func() {
fmt.Println("processando ticket")
}
}
close(trabalhos)
<-pronto
Rodei aqui e os 31 trabalhos levaram 3,25 segundos. Dez por segundo, como pedido.
Mas por que não um Ticker?
Link para o cabeçalho
Dá para escrever a bilheteria com um time.Ticker, que já dispara sozinho a cada intervalo. Esperar o tique e entregar o ticket. Parece a ferramenta certa, neh?
Escrevi essa versão para comparar.
Deixei o trabalhador dormindo 250 milissegundos antes de pegar o primeiro ticket, e anotei quando cada um chegou:
$ go run . ticker
ticket 0 em 250ms (intervalo desde o anterior: 0ms)
ticket 1 em 251ms (intervalo desde o anterior: 1ms)
ticket 2 em 300ms (intervalo desde o anterior: 49ms)
ticket 3 em 400ms (intervalo desde o anterior: 100ms)
Opa! O ticket 1 saiu 1 milissegundo depois do 0.
O Ticker continuou tiquetando enquanto a bilheteria estava parada no envio, e ele guarda um tique no canal. Quando o trabalhador finalmente pegou o ticket 0, a próxima volta do laço encontrou o tique já esperando, e o ticket 1 saiu na hora. Dois de uma vez, e lá se foi o limite.
Com o Timer, o Reset acontece depois da entrega, ou seja, o intervalo conta a partir do momento em que o trabalhador pegou o ticket:
$ go run . timer
ticket 0 em 250ms (intervalo desde o anterior: 0ms)
ticket 1 em 351ms (intervalo desde o anterior: 100ms)
ticket 2 em 451ms (intervalo desde o anterior: 100ms)
ticket 3 em 551ms (intervalo desde o anterior: 100ms)
Logo, por mais lento que o trabalhador seja, nunca saem dois tickets juntos.
Mas e a rajada? Link para o cabeçalho
É aqui que o nome rate limiting fica aproximado.
Esta bilheteria nunca deixa dois saírem juntos, então ela também não permite rajadas. Se o trabalhador ficou dez segundos sem pedir nada, ele não ganhou cem tickets acumulados. Ganhou um.
Um limitador de verdade, o token bucket, começa com um saldo de n fichas e repõe uma a cada janela/n. Dá para gastar as n de uma vez e depois esperar. Se a sua API tolera rajada, o pacote golang.org/x/time/rate faz isso e não vale a pena reinventar.
De onde veio o exemplo Link para o cabeçalho
O código está na pasta ticket do concorrencia-go. O teste de lá roda no synctest com cinco tickets a cada 50 milissegundos, e três trabalhos levam exatos 20 milissegundos, porque na bolha do relógio falso o tempo só anda quando todo mundo está esperando.
No próximo vem o processamento em lote, que junta valores que chegam um a um para processar de uma vez.
Lembrando que o Reset sem drenar o pausa.C só é correto desde o Go 1.23, e com o go.mod declarando go 1.23 ou mais novo. Antes disso um tique velho ficava no canal e o ticket seguinte saía na hora, o mesmo problema do Ticker.
Então é isso, pessoal!
Até a próxima!
{}’s