Skip to main content

Integração com outro serviço

Quando usar: quando o módulo precisa avisar, consultar ou comandar outro serviço. O código conhece nomes lógicos; o endereço, o TLS e a audiência vêm da configuração do ambiente.

1. Declare a dependência​

nns/service.yaml:

dependencies:
- {name: mysql.internal.nnumbers}
- {name: notifier.internal.nnumbers}

Sem a declaração, não existe rede de saída para esse destino: o egress do pod é só o declarado (a política é gerada pelo Module Manager a partir deste arquivo, e o destino precisa estar no catálogo do deployment target). Declarar no service.yaml e não ter o destino no ambiente é o erro mais comum de primeira integração.

2. Tire o endereço do Registry​

java/src/main/java/.../outbound/NotifierClient.java:

/** O nome lógico do destino, como está em `nns/service.yaml` e no Service Registry. */
public static final String REGISTRY_NAME = "notifier.internal.nnumbers";

/** O escopo que o destino exige e que o teto do emissor precisa cobrir. */
static final String SCOPE = "sample:notify";

static final String PATH = "/v1/announcements";
static final int MAX_BODY = 1 << 20;

private final RegistryHttp http;
private final Signer signer;
private final NnsConfig config;

private NotifierClient(RegistryHttp http, Signer signer, NnsConfig config) {
this.http = http;
this.signer = signer;
this.config = config;
}

/**
* O cliente do ambiente: resolve o destino no Registry e passa a usar a identidade da release.
*
* @throws br.com.nnumbers.console.sdk.registry.RegistryException o destino não está no Registry
* deste ambiente
*/
public static NotifierClient of(Registry registry, Signer signer, NnsConfig config) {
return new NotifierClient(
RegistryHttp.of(registry, REGISTRY_NAME, Duration.ofSeconds(15)), signer, config);
}

O Registry e o Signer são beans do SDK: o primeiro é a config de nomes lógicos do ambiente; o segundo é a identidade desta release (nns:<id>), carregada do arquivo de seed.

3. Assine a chamada​

@Override
public void announce(Ctx ctx, String event, Map<String, Object> body) {
byte[] payload;
try {
payload = Json.mapper().writeValueAsBytes(body);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("sample: aviso não serializável", e);
}
Headers h = new Headers();
h.set("Content-Type", "application/json");
h.set("X-Sample-Event", event);
h.set("X-Sample-Signature", "v1=" + sign(payload));
h.set("X-Correlation-Id", correlation(ctx));
h.set("Authorization", "Bearer " + token(correlation(ctx)));
RegistryHttp.Reply reply = http.send(ctx, "POST", http.url(PATH), h, payload, MAX_BODY);
if (reply.status() < 200 || reply.status() > 299) {
throw new IllegalStateException("notifier: status " + reply.status());
}
}

/**
* O token de identidade da chamada: perfil `svc`, `aud` do Registry, escopo exigido pelo destino e
* vida CURTA (o padrão do perfil é 120 s; o teto é 300 s).
*/
String token(String corr) {
return signer.signSvc(http.endpoint().audience(), SCOPE, Map.of("corr", corr), Duration.ofSeconds(120));
}

Cinco decisões que valem a pena notar:

  • Um token NOVO por chamada, com vida curta. Não existe token de serviço de longa duração.
  • A audiência vem do Registry (http.endpoint().audience()), não de uma string no código: é o que permite o mesmo código falar com o destino de desenvolvimento e o de produção.
  • O escopo é o que o destino exige (sample:notify) — pedir escopo a mais só aumenta o poder.
  • O X-Correlation-Id propaga o corr da requisição original: sem ele, um erro no destino não se liga ao pedido do cliente.
  • O corpo vai assinado com a chave do slot (veja Segredo): identidade responde "quem está chamando"; assinatura responde "o corpo é esse mesmo".

O timeout do cliente (15 s acima) e o prazo do Ctx valem juntos: o menor manda.

4. Use com bom senso​

O aviso é melhor-esforço: o destino fora do ar não pode desfazer uma varredura já gravada.

private void announce(Map<String, Object> result, int score) {
try {
notifier.announce(
Ctx.background().withTimeout(Duration.ofSeconds(5)),
"item.scan.finished",
Map.of("item_id", result.get("item_id"), "score", score, "engine", ScanEngine.ENGINE));
} catch (RuntimeException e) {
logger.atWarn()
.addKeyValue("item_id", result.get("item_id"))
.addKeyValue("error_class", e.getClass().getSimpleName())
.log("sample: aviso ao notificador não saiu");
}
}

O cliente é alcançado por uma interface (Notifier), e é ela que o domínio usa: isso é o que permite testar o handler sem rede e mudar a política de falha num lugar só.

Como testar​

O teste sobe um destino de verdade na JVM e confere o que sai — o token é verificado com o verificador do próprio SDK:

@Test
void theCallCarriesAServiceIdentityTokenThisDestinationAccepts() throws Exception {
client().announce(Ctx.background().withTimeout(Duration.ofSeconds(5)), "item.scan.finished",
Map.of("item_id", "0be1d3c6-6f3e-4a6b-a4f5-2a2f2a9f0d10", "score", 75));

String token = seenAuth.get().substring("Bearer ".length());

// O token é verificado com o MESMO verificador do SDK, contra o trust do destino: assinatura,
// perfil, `aud` e `scope` — não basta "tem um Bearer".
Path trust = TestKeys.writeTrust(dir.resolve("trust"),
List.of(new TestKeys.Issuer(ISSUER, "svc", TestKeys.key("nns:sample"), List.of(NotifierClient.SCOPE))));
Verifier verifier = Verifier.of(Trust.load(trust, null, "development", EmbeddedSchemaCheck.INSTANCE));
Token t = verifier.verify(Ctx.background(), token,
Want.of(Profile.SVC, "internal:notifier").withScopes(NotifierClient.SCOPE));
assertThat(t.issuer()).isEqualTo("nns:sample");
assertThat(t.hasScope(NotifierClient.SCOPE)).isTrue();
}

O destino é um HttpServer local, registrado num registry.yaml de teste que o cliente lê como qualquer outro ambiente:

services:
notifier.internal.nnumbers:
url: "http://127.0.0.1:8080"
audience: "internal:notifier"

E o teste de recusa — destino que responde erro falha a chamada (quem chama decide o que fazer):

@Test
void aDestinationThatRefusesFailsTheCall() {
answer = 503;
assertThatThrownBy(() -> client().announce(ctx, "item.scan.finished", Map.of("item_id", "x", "score", 0)))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("503");
}

Erros comuns​

  • Dependência não declarada — a chamada não sai (política de rede) e o erro aparece como dependency_unavailable. Declare em nns/service.yaml e confirme o destino no catálogo do ambiente.
  • Host, IP ou namespace.svc no código — quebra a portabilidade e a revisão de rede: o código conhece o nome lógico, só.
  • Token de vida longa ou reaproveitado entre chamadas — um token por chamada, curto. O teto do perfil svc é 300 s.
  • Escopo largo ("*" ou o escopo do destino inteiro) — peça o escopo que a operação exige.
  • Chamar outro serviço dentro da transação do banco — a transação segura linha e conexão enquanto a rede demora; chame fora dela.
  • Repetir a chamada sem idempotência do outro lado — retry é esperado; o destino tem de suportar a repetição (idempotência ou operação naturalmente idempotente).
  • Deixar a exceção subir sem classificar — o cliente do seu módulo recebe 500 sem texto; decida se é dependency_unavailable retryable ou uma falha de negócio.
  • Assinar o corpo depois de alterá-lo — assine os bytes que você envia, e não serialização diferente da que foi assinada.
  • Achar que a identidade de serviço dá tenant — ela não tem tenant: quem chama é o módulo. Quando a operação precisa do usuário, ela é do serviço chamador via obo, e quem decide é o destino.