This is an automated email from the ASF dual-hosted git repository.

jason810496 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new b6cc81c2944 Buffer TS SDK logger output until the log socket connects 
(#70085)
b6cc81c2944 is described below

commit b6cc81c2944ce87a56596e464868ed50e14ccb08
Author: Guan-Ming Chiu <[email protected]>
AuthorDate: Thu Jul 23 21:40:12 2026 +0800

    Buffer TS SDK logger output until the log socket connects (#70085)
    
    * Buffer TS SDK logger output until the log socket connects
    
    Records emitted between subprocess start and the --logs socket
    connecting were silently dropped; the Go and Java SDKs already buffer
    this window and flush once connected.
    
    * Guard LogChannel.connect() against non-root and repeat calls
    
    Co-authored-by: Jason(Zhe-You) Liu 
<[email protected]>
    
    ---------
    
    Co-authored-by: Jason(Zhe-You) Liu 
<[email protected]>
---
 ts-sdk/src/coordinator/log-channel.ts        | 62 ++++++++++++++++------
 ts-sdk/src/coordinator/runtime.ts            |  7 ++-
 ts-sdk/tests/coordinator/log-channel.test.ts | 77 +++++++++++++++++++++++-----
 3 files changed, 117 insertions(+), 29 deletions(-)

diff --git a/ts-sdk/src/coordinator/log-channel.ts 
b/ts-sdk/src/coordinator/log-channel.ts
index dd3d78ea509..9dd85482c01 100644
--- a/ts-sdk/src/coordinator/log-channel.ts
+++ b/ts-sdk/src/coordinator/log-channel.ts
@@ -50,9 +50,10 @@ const DEFAULT_LOGGER_NAME = "ts-sdk";
 const CLOSE_FLUSH_TIMEOUT_MS = 3_000;
 
 interface LogChannelState {
-  sock: Socket;
+  sock: Socket | null;
   connected: boolean;
   closed: boolean;
+  buffer: string[];
 }
 
 export class LogChannel {
@@ -66,22 +67,39 @@ export class LogChannel {
     this.isRoot = isRoot;
   }
 
-  static async connect(addr: string, name: string = DEFAULT_LOGGER_NAME): 
Promise<LogChannel> {
-    const shared: LogChannelState = {
-      sock: await connectTcp(addr),
-      connected: true,
-      closed: false,
-    };
-    shared.sock.on("error", (err) => {
+  /** Create a root channel with no socket yet; records are buffered
+   *  until {@link LogChannel#connect} flushes them. */
+  static createBuffered(name: string = DEFAULT_LOGGER_NAME): LogChannel {
+    return new LogChannel({ sock: null, connected: false, closed: false, 
buffer: [] }, name, true);
+  }
+
+  /** Connect the socket and flush buffered records, in order. Root only,
+   *  and callable once: a second call would orphan the first socket. */
+  async connect(addr: string): Promise<void> {
+    if (!this.isRoot) {
+      throw new Error(`[${this.name}] connect() is root-only`);
+    }
+    if (this.shared.sock) {
+      throw new Error(`[${this.name}] connect() called more than once`);
+    }
+    const shared = this.shared;
+    const name = this.name;
+    const sock = await connectTcp(addr);
+    shared.sock = sock;
+    shared.connected = true;
+    sock.on("error", (err) => {
       shared.connected = false;
       process.stderr.write(`[${name}] log socket error: ${err.message}\n`);
     });
-    shared.sock.on("close", () => {
+    sock.on("close", () => {
       if (shared.closed || !shared.connected) return;
       shared.connected = false;
       process.stderr.write(`[${name}] log socket closed unexpectedly; further 
logs go to stderr\n`);
     });
-    return new LogChannel(shared, name, true);
+    for (const line of shared.buffer) {
+      sock.write(Buffer.from(line, "utf8"));
+    }
+    shared.buffer = [];
   }
 
   /** Create a sibling logger that shares the underlying socket but
@@ -117,11 +135,16 @@ export class LogChannel {
       timestamp: record.timestamp ?? new Date().toISOString(),
     });
     const payload = line + "\n";
-    if (!this.shared.connected || !this.shared.sock.writable) {
+    const sock = this.shared.sock;
+    if (!sock) {
+      this.shared.buffer.push(payload);
+      return;
+    }
+    if (!this.shared.connected || !sock.writable) {
       process.stderr.write(payload);
       return;
     }
-    this.shared.sock.write(Buffer.from(payload, "utf8"));
+    sock.write(Buffer.from(payload, "utf8"));
   }
 
   debug(event: string, args: Record<string, unknown> = {}): void {
@@ -143,17 +166,26 @@ export class LogChannel {
   async close(): Promise<void> {
     if (!this.isRoot) return;
     this.shared.closed = true;
+    const sock = this.shared.sock;
+    if (!sock) {
+      // never connected: surface buffered records instead of dropping them
+      for (const line of this.shared.buffer) {
+        process.stderr.write(line);
+      }
+      this.shared.buffer = [];
+      return;
+    }
     if (!this.shared.connected) {
-      this.shared.sock.destroy();
+      sock.destroy();
       return;
     }
     return new Promise((resolve) => {
       const timer = setTimeout(() => {
-        this.shared.sock.destroy();
+        sock.destroy();
         resolve();
       }, CLOSE_FLUSH_TIMEOUT_MS);
       timer.unref();
-      this.shared.sock.end(() => {
+      sock.end(() => {
         clearTimeout(timer);
         resolve();
       });
diff --git a/ts-sdk/src/coordinator/runtime.ts 
b/ts-sdk/src/coordinator/runtime.ts
index 5626aae3106..a3a057b4c01 100644
--- a/ts-sdk/src/coordinator/runtime.ts
+++ b/ts-sdk/src/coordinator/runtime.ts
@@ -126,12 +126,15 @@ export async function startCoordinator(opts: 
StartCoordinatorOptions = {}): Prom
   let runtimeAbort: RuntimeAbort | null = null;
 
   try {
-    // Connect log channel first so early failures are captured.
+    // Records emitted before the `--logs` socket connects are buffered and
+    // flushed on connect; if the connect fails, close() dumps them to stderr.
     // Root logger is `ts-sdk`; subsystems use child names (`ts-sdk.runtime`,
     // `ts-sdk.comm`, `ts-sdk.client`) so structlog's ConsoleRenderer prints
     // them as a distinct `[name]` column on the supervisor side.
-    logs = await LogChannel.connect(parsed.logsAddr);
+    logs = LogChannel.createBuffered();
     const runtimeLogs = logs.child("runtime");
+    runtimeLogs.debug("Connecting log socket", { logs_addr: parsed.logsAddr });
+    await logs.connect(parsed.logsAddr);
     const tasks = listRegisteredTasks();
     runtimeLogs.info("Coordinator runtime started", {
       registered_tasks: tasks,
diff --git a/ts-sdk/tests/coordinator/log-channel.test.ts 
b/ts-sdk/tests/coordinator/log-channel.test.ts
index ad85c487ce8..592170f10ae 100644
--- a/ts-sdk/tests/coordinator/log-channel.test.ts
+++ b/ts-sdk/tests/coordinator/log-channel.test.ts
@@ -43,6 +43,13 @@ async function makeServer(): Promise<Fixture> {
   return { server, port, received, sockClosed };
 }
 
+async function connectChannel(addr: string, name?: string): 
Promise<LogChannel> {
+  const channel =
+    name === undefined ? LogChannel.createBuffered() : 
LogChannel.createBuffered(name);
+  await channel.connect(addr);
+  return channel;
+}
+
 function readRecords(received: Buffer[]): Record<string, unknown>[] {
   return Buffer.concat(received)
     .toString("utf8")
@@ -63,7 +70,7 @@ describe("LogChannel", () => {
   });
 
   it("defaults logger name to 'ts-sdk' and auto-stamps timestamp", async () => 
{
-    const ch = await LogChannel.connect(`127.0.0.1:${fx.port}`);
+    const ch = await connectChannel(`127.0.0.1:${fx.port}`);
     ch.info("hello");
     await ch.close();
     await fx.sockClosed;
@@ -82,7 +89,7 @@ describe("LogChannel", () => {
   });
 
   it("accepts a custom root name", async () => {
-    const ch = await LogChannel.connect(`127.0.0.1:${fx.port}`, 
"ts-sdk.runtime");
+    const ch = await connectChannel(`127.0.0.1:${fx.port}`, "ts-sdk.runtime");
     ch.warning("started");
     await ch.close();
     await fx.sockClosed;
@@ -97,7 +104,7 @@ describe("LogChannel", () => {
   });
 
   it("child() creates a hierarchical sibling sharing the socket", async () => {
-    const root = await LogChannel.connect(`127.0.0.1:${fx.port}`);
+    const root = await connectChannel(`127.0.0.1:${fx.port}`);
     const comm = root.child("comm");
     const client = root.child("client");
     expect(comm.loggerName).toBe("ts-sdk.comm");
@@ -115,7 +122,7 @@ describe("LogChannel", () => {
   });
 
   it("children must not close the shared socket", async () => {
-    const root = await LogChannel.connect(`127.0.0.1:${fx.port}`);
+    const root = await connectChannel(`127.0.0.1:${fx.port}`);
     const child = root.child("comm");
     // Child.close() is a no-op. Root.close() ends the socket.
     await child.close();
@@ -133,7 +140,7 @@ describe("LogChannel", () => {
 
   it("handles post-connect socket errors on the root channel", async () => {
     const write = vi.spyOn(process.stderr, "write").mockImplementation(() => 
true);
-    const root = await LogChannel.connect(`127.0.0.1:${fx.port}`);
+    const root = await connectChannel(`127.0.0.1:${fx.port}`);
     root.child("child");
     const sock = (root as unknown as { shared: { sock: net.Socket } 
}).shared.sock;
 
@@ -148,8 +155,54 @@ describe("LogChannel", () => {
     }
   });
 
+  it("buffers records emitted before connect and flushes them in order on 
connect", async () => {
+    const root = LogChannel.createBuffered();
+    const child = root.child("runtime");
+    child.debug("connecting", { attempt: 1 });
+    root.info("still starting");
+
+    await root.connect(`127.0.0.1:${fx.port}`);
+    root.info("connected");
+    await root.close();
+    await fx.sockClosed;
+
+    const records = readRecords(fx.received);
+    expect(records.map((r) => r["event"])).toEqual([
+      "[ts-sdk.runtime] connecting",
+      "[ts-sdk] still starting",
+      "[ts-sdk] connected",
+    ]);
+    expect(records[0]).toMatchObject({ attempt: 1, level: "debug" });
+  });
+
+  it("rejects connect() from a child and a second connect() on the root", 
async () => {
+    const root = await connectChannel(`127.0.0.1:${fx.port}`);
+    try {
+      await 
expect(root.child("runtime").connect(`127.0.0.1:${fx.port}`)).rejects.toThrow(
+        "root-only",
+      );
+      await expect(root.connect(`127.0.0.1:${fx.port}`)).rejects.toThrow("more 
than once");
+    } finally {
+      await root.close();
+      await fx.sockClosed;
+    }
+  });
+
+  it("close() before connect writes buffered records to stderr", async () => {
+    const write = vi.spyOn(process.stderr, "write").mockImplementation(() => 
true);
+    const root = LogChannel.createBuffered();
+    try {
+      root.info("never made it");
+      await root.close();
+      const line = write.mock.calls.find((c) => String(c[0]).includes("never 
made it"))?.[0];
+      expect(String(line)).toContain('"event":"[ts-sdk] never made it"');
+    } finally {
+      write.mockRestore();
+    }
+  });
+
   it("drops records sent after close instead of writing to the ended socket", 
async () => {
-    const root = await LogChannel.connect(`127.0.0.1:${fx.port}`);
+    const root = await connectChannel(`127.0.0.1:${fx.port}`);
     const child = root.child("comm");
     root.info("before close");
     await root.close();
@@ -165,7 +218,7 @@ describe("LogChannel", () => {
 
   it("writes only the error message when error and close fire together", async 
() => {
     const write = vi.spyOn(process.stderr, "write").mockImplementation(() => 
true);
-    const root = await LogChannel.connect(`127.0.0.1:${fx.port}`);
+    const root = await connectChannel(`127.0.0.1:${fx.port}`);
     const sock = (root as unknown as { shared: { sock: net.Socket } 
}).shared.sock;
 
     try {
@@ -182,7 +235,7 @@ describe("LogChannel", () => {
 
   it("falls back to stderr when the socket is no longer writable", async () => 
{
     const write = vi.spyOn(process.stderr, "write").mockImplementation(() => 
true);
-    const root = await LogChannel.connect(`127.0.0.1:${fx.port}`);
+    const root = await connectChannel(`127.0.0.1:${fx.port}`);
     const sock = (root as unknown as { shared: { sock: net.Socket } 
}).shared.sock;
 
     try {
@@ -198,7 +251,7 @@ describe("LogChannel", () => {
   });
 
   it("close() resolves via timeout when the flush never completes", async () 
=> {
-    const root = await LogChannel.connect(`127.0.0.1:${fx.port}`);
+    const root = await connectChannel(`127.0.0.1:${fx.port}`);
     const sock = (root as unknown as { shared: { sock: net.Socket } 
}).shared.sock;
     vi.spyOn(sock, "end").mockImplementation(() => sock);
     const destroy = vi.spyOn(sock, "destroy");
@@ -216,7 +269,7 @@ describe("LogChannel", () => {
 
   it("reports close-time socket errors", async () => {
     const write = vi.spyOn(process.stderr, "write").mockImplementation(() => 
true);
-    const root = await LogChannel.connect(`127.0.0.1:${fx.port}`);
+    const root = await connectChannel(`127.0.0.1:${fx.port}`);
     const sock = (root as unknown as { shared: { sock: net.Socket } 
}).shared.sock;
 
     try {
@@ -241,7 +294,7 @@ describe("LogChannel", () => {
     const port = (server.address() as net.AddressInfo).port;
 
     const write = vi.spyOn(process.stderr, "write").mockImplementation(() => 
true);
-    const root = await LogChannel.connect(`127.0.0.1:${port}`);
+    const root = await connectChannel(`127.0.0.1:${port}`);
     const child = root.child("comm");
     const shared = (root as unknown as { shared: { connected: boolean } 
}).shared;
     try {
@@ -279,7 +332,7 @@ describe("LogChannel", () => {
     const port = (server.address() as net.AddressInfo).port;
 
     const write = vi.spyOn(process.stderr, "write").mockImplementation(() => 
true);
-    const root = await LogChannel.connect(`127.0.0.1:${port}`);
+    const root = await connectChannel(`127.0.0.1:${port}`);
     try {
       await vi.waitFor(() => expect(serverSocks).toHaveLength(1));
       serverSocks[0]!.destroy();

Reply via email to