Pragiva — Tecnologia na prática

Trocar de empresa dentro de uma API: por que RpcSetEnv derruba a thread e o que fazer

RpcSetEnv na mesma thread de uma API REST não troca o ambiente, derruba a conexão; StartJob cria a thread nova para trocar de empresa.

Protheus Guia de processo 5 min de leitura

Assista o vídeo

Resposta rápida

Onde ocorre
dentro do método/função anotada de uma API REST TLPP, quando ela precisa processar empresa ou filial diferente da carregada pelo serviço.
O que NÃO fazer
chamar RpcSetEnv direto na thread da API.
Por quê
a thread já tem ambiente — o PREPAREIN do AppServer carregou — e sobrepor gera inconsistência; o AppServer derruba a thread.
A saída
abrir uma thread nova e vazia com StartJob, e só nela chamar RpcSetEnv.
O que fecha o ciclo
RpcClearEnv no fim dessa thread nova, sempre — ele libera a licença que o RpcSetEnv consumiu.
Nesta página5 seções

Quem sobe uma API REST em TLPP e testa com uma empresa só não vê o problema. Ele aparece quando outro módulo, outra filial ou um cliente com mais de uma empresa chama a mesma rota — e a conexão cai. O sintoma vira reputação: “a API do Protheus cai sozinha”. Não cai sozinha. Cai porque alguém tentou trocar o ambiente na mesma thread que o AppServer já preparou.

A thread da API já nasce com dono

A seção HTTP do appserver.ini tem uma chave chamada PREPAREIN. Ela diz ao AppServer qual empresa e qual filial carregar assim que a thread do serviço sobe — antes de qualquer linha do seu método anotado rodar. Uma classe com @Get/@Post não abre o próprio ambiente: ela herda o que o PREPAREIN preparou, e o oRest injetado já está falando com aquele ambiente.

Isso funciona enquanto a rota só processa a empresa configurada. O problema aparece na rota que recebe empresa e filial no corpo ou na query e precisa processar um ambiente diferente do que o AppServer já carregou. Se a função anotada chama RpcSetEnv para trocar, está pedindo ao ambiente para se sobrepor a si mesmo, na mesma thread — e isso é exatamente o que uma rotina bem escrita nunca faz. RpcClearEnv, o comando que desfaz o que RpcSetEnv abre, existe justamente porque abrir ambiente consome licença e conexões — o inverso exato do que ele libera. Chamar RpcSetEnv sobre um ambiente que já existe é pedir ao AppServer para manter dois ambientes na mesma thread ao mesmo tempo, e ele não mantém.

A separação que resolve: outra thread, sem ambiente

A saída para esse tipo de conflito é específica: não bota a regra de negócio dentro da função que já tem ambiente. Bota numa segunda função, que nasce sem ambiente nenhum, porque é chamada por StartJob — e uma thread aberta por StartJob começa vazia, livre para preparar o que quiser.

ADVPL / TLPP
// Dentro do método/função anotada da API, SÓ dispara a thread
@Post("api/exemplo/v1/troca")
Method TrocarEmpresa() Class ExemploController
    Local jBody := oRest:getBodyRequest()

    StartJob("PGV999Job", GetEnvServer(), .F., jBody["empresa"], jBody["filial"], jBody["dados"])

    oRest:setStatusCode(202)
    oRest:setResponse('{"status":"aceito"}')
Return
ADVPL / TLPP
// Função separada, SEM namespace: é chamada pelo nome, como string
Function PGV999Job(cEmpresa, cFilial, jDados)
    Local lPreparado := (Type("cEmpAnt") != "U")

    If !lPreparado
        RpcSetEnv(cEmpresa, cFilial)
    EndIf

    // a regra de negocio roda aqui, ja no ambiente certo

    If !lPreparado
        RpcClearEnv()
    EndIf
Return

Os parâmetros de StartJob, na ordem em que o código de aplicação costuma usá-los: o nome da função entre aspas — StartJob resolve o nome como string, não como referência de código —, o ambiente do AppServer via GetEnvServer(), um lógico, e depois os parâmetros da própria função, na ordem que ela declara. É um formato que se repete em job de integração e em rotina de fechamento de módulo, sempre na mesma ordem.

O lógico não é liga/desliga de background: ele decide se você recebe retorno. .T. faz StartJob aguardar a thread nova terminar e devolver o que a função retornou; é assim que uma rotina síncrona captura uRet := StartJob(...) para responder ao chamador. .F. dispara a thread e segue sem esperar, útil quando o processo é longo e o chamador só precisa confirmar que o disparo aconteceu. Numa API que só confirma que o processamento começou, como no exemplo acima, que devolve 202 antes de saber o resultado, .F. é a escolha certa; numa que devolve o resultado no mesmo corpo de resposta, é .T..

O teste que evita preparar duas vezes

Type("cEmpAnt"), o nome da variável entre aspas, porque é isso que Type() espera receber, devolve "U" (indefinido) quando a thread ainda não tem ambiente nenhum. É esse teste que webservice bem escrito costuma usar antes de chamar RpcSetEnv, entrando no bloco só quando Type('cEmpAnt') == 'U'. Numa thread criada por StartJob, Type("cEmpAnt") será "U" na primeira execução, sempre: a thread nasceu vazia. O teste segue útil se a mesma função um dia for chamada por outro caminho que já prepare ambiente antes.

Por que a função do job não pode ter namespace

PGV999Job, acima, é uma Function comum, fora de qualquer namespace. Não é escolha de estilo: StartJob recebe o nome da função como texto e resolve esse texto no runtime, do mesmo jeito que ExecBlock resolve uma User Function pelo nome. Uma função dentro de um namespace só existe, para quem chama de fora, pelo nome totalmente qualificado, e é esse tipo de resolução por string que quebra quando o nome curto não aponta para nada. Por isso, mesmo num arquivo TLPP que importa vários namespace no topo com using namespace, a função exposta para StartJob costuma ficar de propósito solta, sem namespace próprio: é o alvo de uma chamada por nome.

Depurar isso exige ligar outra chave no TDS

StartJob não executa a função na sua thread: ele cria uma thread independente no AppServer e é ela quem roda PGV999Job. Um ponto de parada plantado dentro dessa função só é atingido se a depuração de múltiplas threads estiver habilitada no TDS antes de a chamada acontecer; com a depuração de thread única, a execução simplesmente segue, sem parar, porque o depurador está de olho na thread que fez a chamada, não na que StartJob acabou de abrir.

Uma API que recebe empresa e filial por parâmetro não precisa, e não deve, tentar reabrir o próprio ambiente. Precisa delegar essa troca para uma thread que nunca teve ambiente nenhum, e fechar essa thread do jeito que abriu: com RpcClearEnv devolvendo a licença que RpcSetEnv pediu emprestada.


Se a API que você mantém está derrubando thread ao trocar de empresa, fale com a gente.

Referências

  1. TDNtdn.totvs.com

    TDN — StartJob
  2. TDNtdn.totvs.com

    TDN — RpcSetEnv, abertura de ambiente em rotinas automáticas

Continue por aqui