un

guest
1 / ?
back to lessons

ThreadLocal: Idioma Correto, Era Errada

Java EE Servlet containers, cerca de 1999: um thread por solicitação. Um thread trata exatamente uma solicitação de começar ao fim, depois termina. ThreadLocal armazena um valor associado ao thread atual. Com um-thread-per-request, um valor armazenado em ThreadLocal pertence a exatamente uma solicitação. O idioma: correto.

Pool de threads mudou o contrato. Um thread trata solicitação A, armazena principal A em ThreadLocal, termina solicitação A e retorna para o pool. Pool de threads não reinicia o estado do thread. ThreadLocal.remove() limpa, mas chamá-lo requer disciplina explícita. Quando a disciplina falha, solicitação B roda no mesmo thread e lê principal A em ThreadLocal.

A 5-passo vazamento:

1. Solicitação A chega. O servidor atribui Thread-7.

2. Thread-7 define ThreadLocal.set(principal_A) no início da solicitação.

3. Solicitação A completa. Thread-7 retorna para o pool. ThreadLocal.remove() não é chamado.

4. Solicitação B chega. O servidor atribui Thread-7 (reuso do pool).

5. Thread-7 lê ThreadLocal.get(): retorna principal_A. Solicitação B roda com a identidade errada.

Por Que Os Testes O Perdem

Testes unitários correm em isolamento: nenhum pool de threads, nenhum reuso. Testes de integração usam threads frescas ou reinicia o estado entre os testes. Testes de carga aquecem com usuários corretos e baixa concorrência. O defeito só se manifesta com o reuso de pool de threads e solicitações sobrepôs, uma condição que aparece em produção sob tráfego normal, não em qualquer configuração de teste que verifica isso.

A Consequência de Segurança

O principal do usuário A se derrama na solicitação do usuário B. Não é um crash. Não é uma exceção. Uma violação silenciosa da fronteira de segurança: o usuário B executa ações como usuário A, lê dados do usuário A ou bypassa as permissões do usuário B. O sistema produz nenhum erro. Os logs mostram que a solicitação B foi autorizada. Tudo parece correto.

Os Cinco Passos

Os cinco passos de um vazamento ThreadLocal importam precisamente: o defeito não ocorre no momento que o código errado roda. Ele ocorre mais cedo, na ausência de um passo de limpeza.

Passe pelos 5 passos. Em qual passo ocorre o defeito e por que um suite de testes o perderia?

Valores Associados ao Escopo

ThreadLocal associa um valor a uma thread. Uma thread vive além de uma solicitação. Colisão.

Valores associados ao escopo associam um valor a uma unidade de trabalho. Quando a unidade de trabalho termina, o valor também termina. Nenhuma limpeza explícita. Nenhuma remove() para esquecer.

Java 21: ScopedValue

// ThreadLocal (carro de defeito)
static final ThreadLocal<Principal> PRINCIPAL = new ThreadLocal<>();
PRINCIPAL.set(principal);           // definido no início da solicitação
// ... manipulação da solicitação ...
PRINCIPAL.remove();                 // DEVE ser chamado; frequentemente esquecido

// ScopedValue (carro correto)
static final ScopedValue<Principal> PRINCIPAL = ScopedValue.newInstance();
ScopedValue.where(PRINCIPAL, principal).run(() -> {
    // ... manipulação da solicitação ...
    // o valor é automaticamente perdido quando a run() retorna
});

Go: context.Context

// context.Context carrega valores explicitamente; escopo = cadeia de chamada de funções
ctx := context.WithValue(r.Context(), principalKey, principal)
handleRequest(ctx)  // ctx passado explicitamente; perdido quando a função retorna

Python asyncio: contextvars.ContextVar

# ContextVar escopo para cada tarefa assíncrona
PRINCIPAL: ContextVar[str] = ContextVar('principal')
token = PRINCIPAL.set(principal)    # definido somente para essa tarefa
# ... manipulação de tarefas ...
PRINCIPAL.reset(token)              # ou: o escopo termina com a tarefa

A propriedade que eles compartilham: o tempo de vida corresponde à unidade de trabalho. Quando a solicitação termina (a run() retorna, a função retorna, a tarefa é concluída), o valor também termina. Nenhuma limpeza para esquecer. Nenhuma piscina para corromper.

Identificar e Substituir

Uma aplicação Java EE armazena o ID do inquilino em um ThreadLocal no início da solicitação. Sob alto volume de carga, o ID do inquilino A aparece em solicitações do inquilino B. As consultas do inquilino B retornam dados do inquilino A. Nenhuma exceção é lançada. O defeito só aparece no teste de carga em produção.

O que MOAD descreve isso? Qual operadora fez o defeito possível? O que o substitui e qual propriedade da substituição impede a vazamento?