Corrigiu replica via sleepApp

X-Lovable-Edit-ID: edt-d299c2dd-2ed1-4767-ae85-143c3feababf
Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot] 2026-04-23 18:56:08 +00:00
commit 50a0c9042e
3 changed files with 87 additions and 24 deletions

View file

@ -137,12 +137,19 @@ export async function deployRailwayService(opts: {
} }
} }
/**
* Suspende ou retoma um serviço Railway usando `sleepApplication`.
* Railway não aceita `numReplicas: 0` via serviceInstanceUpdate o caminho
* suportado para pausar é `sleepApplication: true` (e `false` para acordar).
* `replicas <= 0` => sleep, `>= 1` => wake.
*/
export async function setRailwayReplicas(opts: { export async function setRailwayReplicas(opts: {
token: string; token: string;
serviceId: string; serviceId: string;
environmentId: string; environmentId: string;
replicas: number; replicas: number;
}): Promise<void> { }): Promise<void> {
const sleep = opts.replicas <= 0;
const mutation = ` const mutation = `
mutation ServiceInstanceUpdate($serviceId: String!, $environmentId: String!, $input: ServiceInstanceUpdateInput!) { mutation ServiceInstanceUpdate($serviceId: String!, $environmentId: String!, $input: ServiceInstanceUpdateInput!) {
serviceInstanceUpdate(serviceId: $serviceId, environmentId: $environmentId, input: $input) serviceInstanceUpdate(serviceId: $serviceId, environmentId: $environmentId, input: $input)
@ -153,13 +160,49 @@ export async function setRailwayReplicas(opts: {
{ {
serviceId: opts.serviceId, serviceId: opts.serviceId,
environmentId: opts.environmentId, environmentId: opts.environmentId,
input: { numReplicas: opts.replicas }, input: { sleepApplication: sleep },
}, },
opts.token, opts.token,
); );
if (res.errors?.length) { if (res.errors?.length) {
throw new Error(`setRailwayReplicas failed: ${JSON.stringify(res.errors)}`); throw new Error(`setRailwayReplicas (sleepApplication=${sleep}) failed: ${JSON.stringify(res.errors)}`);
} }
// Força redeploy para aplicar imediatamente o novo estado de sleep
try {
await deployRailwayService({
token: opts.token,
serviceId: opts.serviceId,
environmentId: opts.environmentId,
});
} catch (e) {
console.warn("setRailwayReplicas: redeploy after sleep change failed (non-fatal):", e);
}
}
/** Busca o environmentId do primeiro deployment de um serviço. Útil quando vps_pool_id está null. */
export async function getServiceEnvironmentId(opts: {
token: string;
serviceId: string;
}): Promise<string | null> {
const query = `
query Service($id: String!) {
service(id: $id) {
projectId
deployments(first: 1) {
edges { node { environmentId } }
}
}
}
`;
const res = await railwayQuery<{
service: { projectId: string; deployments: { edges: { node: { environmentId: string } }[] } };
}>(query, { id: opts.serviceId }, opts.token);
if (res.errors?.length) {
console.error("getServiceEnvironmentId errors:", JSON.stringify(res.errors));
return null;
}
return res.data?.service?.deployments?.edges?.[0]?.node?.environmentId ?? null;
} }
/** Apaga o webhook do Telegram para que o Hermes assuma via polling. */ /** Apaga o webhook do Telegram para que o Hermes assuma via polling. */

View file

@ -5,7 +5,7 @@
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.4"; import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.4";
import { corsHeaders } from "../_shared/cors.ts"; import { corsHeaders } from "../_shared/cors.ts";
import { setRailwayReplicas } from "../_shared/railway.ts"; import { setRailwayReplicas, getServiceEnvironmentId } from "../_shared/railway.ts";
const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!; const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!;
const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
@ -45,7 +45,7 @@ Deno.serve(async (req) => {
if (!agent) return jsonResponse(404, { error: "agent_instance not found" }); if (!agent) return jsonResponse(404, { error: "agent_instance not found" });
if (!agent.railway_service_id || !agent.vps_pool_id) { if (!agent.railway_service_id) {
return jsonResponse(409, { return jsonResponse(409, {
error: "agent has no container — needs full provisioning instead", error: "agent has no container — needs full provisioning instead",
}); });
@ -55,21 +55,31 @@ Deno.serve(async (req) => {
return jsonResponse(200, { ok: true, already_active: true }); return jsonResponse(200, { ok: true, already_active: true });
} }
// Resolve environmentId: prioriza vps_pool, fallback para query no Railway
let environmentId: string | null = null;
if (agent.vps_pool_id) {
const { data: pool } = await supabase const { data: pool } = await supabase
.from("vps_pool") .from("vps_pool")
.select("railway_environment_id") .select("railway_environment_id")
.eq("id", agent.vps_pool_id) .eq("id", agent.vps_pool_id)
.maybeSingle(); .maybeSingle();
environmentId = pool?.railway_environment_id ?? null;
if (!pool?.railway_environment_id) { }
return jsonResponse(500, { error: "pool environment not configured" }); if (!environmentId) {
environmentId = await getServiceEnvironmentId({
token: RAILWAY_API_TOKEN,
serviceId: agent.railway_service_id,
});
}
if (!environmentId) {
return jsonResponse(500, { error: "could not resolve railway environmentId" });
} }
try { try {
await setRailwayReplicas({ await setRailwayReplicas({
token: RAILWAY_API_TOKEN, token: RAILWAY_API_TOKEN,
serviceId: agent.railway_service_id, serviceId: agent.railway_service_id,
environmentId: pool.railway_environment_id, environmentId,
replicas: 1, replicas: 1,
}); });
} catch (e) { } catch (e) {

View file

@ -5,7 +5,7 @@
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.4"; import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.4";
import { corsHeaders } from "../_shared/cors.ts"; import { corsHeaders } from "../_shared/cors.ts";
import { setRailwayReplicas } from "../_shared/railway.ts"; import { setRailwayReplicas, getServiceEnvironmentId } from "../_shared/railway.ts";
const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!; const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!;
const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
@ -47,7 +47,7 @@ Deno.serve(async (req) => {
if (agent.status === "suspended") { if (agent.status === "suspended") {
return jsonResponse(200, { ok: true, already_suspended: true }); return jsonResponse(200, { ok: true, already_suspended: true });
} }
if (!agent.railway_service_id || !agent.vps_pool_id) { if (!agent.railway_service_id) {
// Sem container provisionado ainda — só marca o status // Sem container provisionado ainda — só marca o status
await supabase await supabase
.from("agent_instances") .from("agent_instances")
@ -56,21 +56,31 @@ Deno.serve(async (req) => {
return jsonResponse(200, { ok: true, no_container: true }); return jsonResponse(200, { ok: true, no_container: true });
} }
// Resolve o environmentId: prioriza vps_pool, fallback para query no Railway
let environmentId: string | null = null;
if (agent.vps_pool_id) {
const { data: pool } = await supabase const { data: pool } = await supabase
.from("vps_pool") .from("vps_pool")
.select("railway_environment_id") .select("railway_environment_id")
.eq("id", agent.vps_pool_id) .eq("id", agent.vps_pool_id)
.maybeSingle(); .maybeSingle();
environmentId = pool?.railway_environment_id ?? null;
if (!pool?.railway_environment_id) { }
return jsonResponse(500, { error: "pool environment not configured" }); if (!environmentId) {
environmentId = await getServiceEnvironmentId({
token: RAILWAY_API_TOKEN,
serviceId: agent.railway_service_id,
});
}
if (!environmentId) {
return jsonResponse(500, { error: "could not resolve railway environmentId" });
} }
try { try {
await setRailwayReplicas({ await setRailwayReplicas({
token: RAILWAY_API_TOKEN, token: RAILWAY_API_TOKEN,
serviceId: agent.railway_service_id, serviceId: agent.railway_service_id,
environmentId: pool.railway_environment_id, environmentId,
replicas: 0, replicas: 0,
}); });
} catch (e) { } catch (e) {