Uma fila de eventos em que cada evento vira um INSERT. Mil eventos por segundo, mil idas e voltas ao banco por segundo.

O banco aceita cem linhas em uma requisição só. Mas os eventos não chegam de cem em cem, chegam um a um.

O que é um processamento em lote Link para o cabeçalho

Um processamento em lote agrupa itens que chegam um por um, para que o consumidor os processe em blocos.

Ele também aparece como batching ou micro-batching.

Um lote sai de três jeitos. Quando enche, quando passa um prazo sem encher, para um item não ficar esperando companhia para sempre, e sob demanda, por um canal de descarga.

func processamentoLotes(entrada <-chan req, descarga <-chan struct{}, tamanhoLote int, intervalo time.Duration) <-chan []req {
	saida := make(chan []req)
	go func() {
		defer close(saida)
		buf := make([]req, 0, tamanhoLote)

		// nasce parado, porque ainda não existe lote
		prazo := time.NewTimer(intervalo)
		prazo.Stop()

		descarregar := func() {
			if len(buf) == 0 {
				return
			}
			prazo.Stop()
			saida <- buf
			buf = make([]req, 0, tamanhoLote)
		}

		for {
			select {
			case item, ok := <-entrada:
				if !ok {
					// entrada fechada: o lote parcial ainda sai
					descarregar()
					return
				}
				// primeiro item do lote: começa a contar o prazo
				if len(buf) == 0 {
					prazo.Reset(intervalo)
				}
				buf = append(buf, item)
				if len(buf) == tamanhoLote {
					descarregar()
				}
			case <-prazo.C:
				descarregar()
			case <-descarga:
				descarregar()
			}
		}
	}()
	return saida
}

A main pede lotes de três itens ou 100 milissegundos, o que vier primeiro, e testa os três jeitos:

saida := processamentoLotes(entrada, descarga, 3, 100*time.Millisecond)
pronto := processadorLotes(saida)

// três itens enchem o lote
entrada <- req{valor: 1}
entrada <- req{valor: 2}
entrada <- req{valor: 3}

// dois itens e uma descarga manual
entrada <- req{valor: 4}
entrada <- req{valor: 5}
descarga <- struct{}{}

// um item sozinho, e o prazo passa
entrada <- req{valor: 6}
time.Sleep(150 * time.Millisecond)

// dois itens, e fechar a entrada descarrega o resto
entrada <- req{valor: 7}
entrada <- req{valor: 8}
close(entrada)
<-pronto

O processadorLotes é o trabalhador de sempre, lendo um []req por vez e fechando pronto no fim.

$ go run .
processando lote com valores: [{1} {2} {3}]
processando lote com valores: [{4} {5}]
processando lote com valores: [{6}]
processando lote com valores: [{7} {8}]

Preste atenção no item 6. Ele saiu sozinho, sem encher o lote e sem ninguém pedir, porque os 100 milissegundos passaram.

Repare também no prazo.Reset dentro do if len(buf) == 0. O prazo conta a partir do primeiro item de cada lote, e não do relógio da parede. Um time.Ticker marcaria o tempo por conta própria, e um lote começado logo antes do tique sairia com um item. É o mesmo motivo do sistema de ticket.

Mas por que não reaproveitar o slice? Link para o cabeçalho

Olhe o buf = make([]req, 0, tamanhoLote) depois do envio. Um make a cada lote. Dá vontade de trocar por buf = buf[:0], que zera o tamanho e mantém a memória.

Eu troquei. E para ficar parecido com um banco de verdade, deixei o consumidor demorar 50 milissegundos entre receber o lote e ler o conteúdo:

$ go run . reaproveitar
processando lote com valores: [{4} {5} {6}]
processando lote com valores: [{4} {5} {6}]

Opa! O lote [{4} {5} {6}] apareceu duas vezes, e os itens 1, 2 e 3 sumiram.

Um slice é só uma janela para um array por baixo. O saida <- buf entregou a janela, mas o buf[:0] continuou apontando para o mesmo array, e os append seguintes escreveram por cima do que o consumidor ainda não tinha lido.

Com o make, cada lote tem o seu array, e o consumidor pode demorar o quanto quiser. É o preço de passar um slice por um canal.

De onde veio o exemplo Link para o cabeçalho

O código está na pasta lote do concorrencia-go. O teste roda a main inteira dentro do synctest, então o time.Sleep de 150 milissegundos não custa nada.

No próximo vem a janela deslizante, a que ficou devendo lá na contrapressão. Quando o consumidor não acompanha, em vez de frear o produtor, ela descarta o valor mais antigo.

Lembre-se que o prazo.Stop() sem drenar o prazo.C só é correto desde o Go 1.23. Antes disso, se o lote enchesse no mesmo instante do disparo, o tique ficava guardado e o lote seguinte sairia cedo.

Então é isso, pessoal!

Até a próxima!

{}’s