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
RpcSetEnvdireto na thread da API. - Por quê
- a thread já tem ambiente — o
PREPAREINdo 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 chamarRpcSetEnv. - O que fecha o ciclo
RpcClearEnvno fim dessa thread nova, sempre — ele libera a licença que oRpcSetEnvconsumiu.
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.
// 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// 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
ReturnOs 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
TDNtdn.totvs.com
TDN — StartJobTDNtdn.totvs.com
TDN — RpcSetEnv, abertura de ambiente em rotinas automáticas
Continue por aqui
- Vídeo
ProtheusTutorial
Paginação com FWAdapterBaseV2: a classe que abstrai o banco
FWAdapterBaseV2 abstrai a sintaxe de paginação de SQL Server, Oracle e Postgres. AddMapFields, não SetFieldMap, é o método que mapeia cada campo.
- Vídeo
ProtheusGuia de processo
Erro esperado e erro inesperado numa API TLPP
setFault recebe um parâmetro só, sem código HTTP: por isso o erro de negócio some atrás de um 500 genérico. Os dois tratamentos numa API TLPP.