ydb-nodejs-sdk icon indicating copy to clipboard operation
ydb-nodejs-sdk copied to clipboard

SeqNo is not properly initialized on topic writer creation

Open nikolaymatrosov opened this issue 4 months ago • 2 comments

Here is the PR with demo code that I hope will help easily replicate the issue.

In the README there is a code to test the serverless producer function. It makes 4 consecutive call to the function and in the output prints the sequenceNumber it gets from the writer. In all 4 call I got 1. If I check the stream in the data base, I see only first message.

I have tried writer and writer2 implementations. They both have same issue.

Additional info

Code snippet

  await driver.ready();

  // Create topic writer using the documented API
  await using writer = createTopicWriter(driver, {
    topic: ydsTopicPath,
    producer: "producer-ts",
  });

  // Write message to topic
  const messageData = JSON.stringify(message);

  const seqNo = writer.write(
    new TextEncoder().encode(messageData)
  );

  await writer.flush();

  return {
    statusCode: 200,
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      status: "success",
      message: "Message sent to YDS topic",
      data: message,
      sequenceNumber: seqNo.toString(),
    }),
  };

The test code:

# Populate PRODUCER_URL from terraform outputs (use -raw to avoid quotes)
export PRODUCER_URL=$(terraform output -raw producer_function_url)
echo "Producer URL set to: $PRODUCER_URL"

# Send login event
echo "Sending login event for user123..."
curl -sS -X POST "$PRODUCER_URL" \
  -H "Content-Type: application/json" \
  -d '{"message": "User logged in", "user_id": "user123", "action": "login"}' \
  | jq . || echo "Non-JSON or empty response"

# Send purchase event
echo "Sending purchase event for user123..."
curl -sS -X POST "$PRODUCER_URL" \
  -H "Content-Type: application/json" \
  -d '{"message": "Purchased item XYZ", "user_id": "user123", "action": "purchase"}' \
  | jq . || echo "Non-JSON or empty response"

# Send view event
echo "Sending view event for user456..."
curl -sS -X POST "$PRODUCER_URL" \
  -H "Content-Type: application/json" \
  -d '{"message": "Viewed product ABC", "user_id": "user456", "action": "view"}' \
  | jq . || echo "Non-JSON or empty response"

# Send logout event
echo "Sending logout event for user123..."
curl -sS -X POST "$PRODUCER_URL" \
  -H "Content-Type: application/json" \
  -d '{"message": "User logged out", "user_id": "user123", "action": "logout"}' \
  | jq . || echo "Non-JSON or empty response"

The output:

Sending login event for user123...
{
  "status": "success",
  "message": "Message sent to YDS topic",
  "data": {
    "message": "User logged in",
    "user_id": "user123",
    "action": "login",
    "timestamp": "2025-11-02T13:42:11.433Z"
  },
  "sequenceNumber": "1"
}
Sending purchase event for user123...
{
  "status": "success",
  "message": "Message sent to YDS topic",
  "data": {
    "message": "Purchased item XYZ",
    "user_id": "user123",
    "action": "purchase",
    "timestamp": "2025-11-02T13:42:12.968Z"
  },
  "sequenceNumber": "1"
}
Sending view event for user456...
{
  "status": "success",
  "message": "Message sent to YDS topic",
  "data": {
    "message": "Viewed product ABC",
    "user_id": "user456",
    "action": "view",
    "timestamp": "2025-11-02T13:42:13.500Z"
  },
  "sequenceNumber": "1"
}
Sending logout event for user123...
{
  "status": "success",
  "message": "Message sent to YDS topic",
  "data": {
    "message": "User logged out",
    "user_id": "user123",
    "action": "logout",
    "timestamp": "2025-11-02T13:42:13.955Z"
  },
  "sequenceNumber": "1"
}

nikolaymatrosov avatar Nov 02 '25 13:11 nikolaymatrosov

@nikolaymatrosov I think I've found a logical inconsistency.

How the sdk gets a seqno

this.#actor.on('writer.session', (event) => {
  this.#seqNoManager.initialize(event.lastSeqNo)
})

How it uses

let seqNo = this.#seqNoManager.getNext(extra?.seqNo)

I think I've found a logical inconsistency. the problem is that when you start writing, the session has not been created yet, and when you call write, it gets 0.

I have a solution, create a promise like .ready() which will be resolved when the session is ready. But I don't want to add unnecessary methods, I'll think about what else I can do.

polRk avatar Nov 04 '25 21:11 polRk

At the moment, the simplest solution is to add a small delay before write.

polRk avatar Nov 04 '25 21:11 polRk

Creating a topic writer is already an async operation — could we make it resolve only after the session is fully initialized?

nikolaymatrosov avatar Nov 05 '25 06:11 nikolaymatrosov

Creating a topic writer is already an async operation

No, it's not.

polRk avatar Nov 05 '25 13:11 polRk

UPD: My bad. I finally read the doc.

Its syntax may be somewhat confusing, because the await does not have an awaiting effect when the variable is first declared, but only when the variable goes out of scope.


So why do I then have to use await in this statement?

await using writer = createTopicWriter(driver, {
    topic: ydsTopicPath,
    producer: "producer-ts",
  });

I took it from package readme.

nikolaymatrosov avatar Nov 05 '25 13:11 nikolaymatrosov

So why do I then have to use await in this statement?

Simple way for making cleanup via Async Disposable - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/asyncDispose https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/await_using

polRk avatar Nov 06 '25 19:11 polRk

@nikolaymatrosov https://github.com/ydb-platform/ydb-js-sdk/pull/545 I'm still testing

polRk avatar Nov 06 '25 22:11 polRk