Pular para o conteúdo

Contrapressão

Quando o produtor é mais rápido que o consumidor — e por que uma fila sem teto é uma falha adiada.

Todo sistema com produtor e consumidor tem uma pergunta que precisa de resposta: o que acontece quando o produtor é mais rápido? Há três respostas possíveis, e escolher por omissão significa escolher a pior.

RespostaO que custaQuando serve
a fila cresce sem tetomorte por memória, horas depoisnunca — é a escolha por omissão
o produtor esperaa lentidão sobe para quem produzquase sempre: é a contrapressão
descarta o mais velhoperde dado, de propósito e visivelmentetelemetria, métrica, quadro de vídeo
dataforge
adopt Arcane.Concurrent as C

// Um canal com TETO: o 'enviar' espera quando ele está cheio, e é
// isso que faz a lentidão do consumidor chegar ao produtor.
canal := C.canal(3)

produzidos := []
action produtor():
    cycle i from 1 to 5:
        canal.enviar(i)
        produzidos.append(i)
    canal.fechar()

action consumidor():
    recebidos := []
    cycle item in canal:
        recebidos.append(item)
        sleep(10)
    yield recebidos

resultado := void
parallel:
    produtor()
    thread:
        resultado := consumidor()

assert resultado is [1, 2, 3, 4, 5]
out $"produziu {len(produzidos)}, consumiu {len(resultado)} — sem perder nada"

A fila sem teto é uma falha adiada#

Ela não dá erro: ela funciona em todos os testes, porque num teste o consumidor acompanha. Em produção, a fila cresce nas horas de pico, a memória acaba de madrugada, e o processo é morto pelo sistema — sem mensagem, e longe da causa.

dataforge
adopt Arcane.Concurrent as C

// Um teto pequeno, de propósito, para a espera aparecer:
canal := C.canal(1)
canal.enviar("a")
assert canal.cheio() is yes

// 'tentar_enviar' NÃO espera: ele devolve no quando não cabe. É o que
// permite ao produtor decidir — esperar, descartar ou contar.
assert canal.tentar_enviar("b") is no

descartados := 0
given not canal.tentar_enviar("c"):
    descartados += 1
assert descartados is 1
out "cheio: o produtor decide o que fazer, em vez de a memória decidir"

Descartar, quando descartar é a resposta certa#

Para telemetria, a leitura de trinta segundos atrás não tem valor: mandar a mais nova e perder a velha é melhor que atrasar as duas. O importante é que isso seja uma decisão escrita, e contada:

dataforge
adopt Arcane.Concurrent as C

blueprint Telemetria:
    action setup(teto):
        self.canal := C.canal(teto)
        self.descartados := 0

    action medir(valor):
        given not self.canal.tentar_enviar(valor):
            // Contar o descarte é o que separa "escolha" de "defeito":
            // sem o número, ninguém sabe que está perdendo dado.
            self.descartados := self.descartados + 1
            yield no
        yield yes

t := spawn Telemetria(2)
assert t.medir(1) is yes
assert t.medir(2) is yes
assert t.medir(3) is no
assert t.descartados is 1
out $"descartados: {t.descartados} — e o número aparece no painel"