Working with coordination nodes
This article describes how to use YDB SDK to coordinate multiple instances of a client application by using coordination nodes and the semaphores they contain.
Creating a coordination node
Coordination nodes are created in YDB databases in the same namespace as other schema objects, such as tables and topics.
TClient client(driver);
auto status = client
.CreateNode("/path/to/mynode")
.ExtractValueSync();
Y_ABORT_UNLESS(status.IsSuccess());
When creating, you can optionally specify TNodeSettings with the following settings:
-
ReadConsistencyMode- defaultRELAXED, which allows reading a not‑most‑fresh value when the leader changes. Optionally you can enableSTRICTread mode, where all reads go through the consensus algorithm and guarantee returning the most recent value, but become significantly more expensive. -
AttachConsistencyMode- defaultSTRICT, which requires using the consensus algorithm when restoring a session. Optionally you can enableRELAXEDsession‑recovery mode in case of failures, which disables this requirement. A relaxed mode may be needed with a very large number of clients, allowing session restoration without going through consensus, which does not affect overall correctness but may increase reading stale values during leader changes and also cause session expiration in case of issues. -
SelfCheckPeriod(default 1 second) – the interval at which the service checks its own liveness. Changing it is not recommended except in special cases.- The larger the specified value, the lower the load on the server, but the longer the possible delay between a leader change and how quickly the service learns about it.
- The smaller the specified value, the higher the load on the server and the greater responsiveness in detecting problems, but false positives may be generated when the service mistakenly detects issues.
-
SessionGracePeriod(default 10 seconds) – the period during which a new leader does not close open sessions, extending them.- The smaller the value, the narrower the window during which sessions from non‑existent clients, which did not report their disappearance during a leader change, will hold semaphores and block other clients.
- The smaller the value, the higher the chance of false triggers, where a live leader may shut down as a precaution because it cannot be sure that this period has not elapsed on the new leader.
- It must be strictly greater than
SelfCheckPeriod.
err := db.Coordination().CreateNode(ctx,
"/path/to/mynode",
)
To work with coordination nodes, add the Maven artifact ydb-sdk-coordination (module tech.ydb.coordination.*). Coordination nodes are needed when multiple instances of an application must coordinate access to resources – see the coordination node section for more details.
Below is a complete example: connecting via GrpcTransport, creating a node, and checking via describeNode.
import tech.ydb.coordination.CoordinationClient;
import tech.ydb.coordination.description.NodeConfig;
import tech.ydb.core.grpc.GrpcTransport;
public class CreateCoordinationNodeExample {
private static final String NODE_PATH_SUFFIX = "/path/to/mynode";
public static void main(String[] args) {
// Connection string from environment variable or default local YDB
String connectionString = System.getenv().getOrDefault(
"YDB_CONNECTION_STRING", "grpc://localhost:2136/local");
try (GrpcTransport transport = GrpcTransport.forConnectionString(connectionString).build();
CoordinationClient client = CoordinationClient.newClient(transport).build()) {
// Full path to the node = database path + node name in the namespace
String nodePath = client.getDatabase() + NODE_PATH_SUFFIX;
// Create a coordination node with default settings
client.createNode(nodePath).join().expectSuccess("не удалось создать узел");
// Verify that the node is created: read its configuration
NodeConfig config = client.describeNode(nodePath).join().getValue();
System.out.println("Узел создан: " + nodePath);
System.out.println("SelfCheckPeriod: " + config.getSelfCheckPeriod());
System.out.println("SessionGracePeriod: " + config.getSessionGracePeriod());
}
}
}
If needed, set the node configuration via NodeConfig, using the NodeConfig.create().with… chain. Available parameters: periods SelfCheckPeriod and SessionGracePeriod, read and session-connection consistency modes (readConsistencyMode, attachConsistencyMode), rate-limiter counters mode (rateLimiterCountersMode). Default values match the description for C++ (see above). The prepared NodeConfig is passed to CoordinationNodeSettings and createNode(nodePath, settings).
Additionally, alterNode (configuration change) and dropNode (node removal) are available.
import ydb
client = driver.coordination_client
client.create_node("/path/to/mynode")
import ydb
client = driver.coordination_client
await client.create_node("/path/to/mynode")
This functionality is not currently supported.
import { CoordinationClient } from "@ydbjs/coordination";
let client = new CoordinationClient(driver);
await client.createNode("/path/to/mynode", {});
The coordination client is returned from Client::coordination_client. A node is created via CoordinationClient::create_node with a path and NodeConfig (via NodeConfigBuilder). Also available are alter_node, drop_node, and describe_node. Full example — mutex.rs.
use ydb::NodeConfigBuilder;
let mut coordination_client = client.coordination_client();
coordination_client
.create_node(
"/path/to/mynode".into(),
NodeConfigBuilder::default().build()?,
)
.await?;
This functionality is not currently supported.
Working with sessions
Creating a session
To get started, the client must establish a session within which it will perform all operations with the coordination node.
TClient client(driver);
const TSession& session = client
.StartSession("/path/to/mynode")
.ExtractValueSync()
.ExtractResult();
When establishing a session, you can optionally pass a TSessionSettings structure with the following settings:
-
Description– a textual description of the session, displayed in internal interfaces and useful for diagnosing problems. -
OnStateChanged– called on important changes during the session’s lifecycle, passing the corresponding state:ATTACHED– the session is connected and operating in normal mode.DETACHED– the session temporarily lost connection to the service but can still be restored.EXPIRED– the session lost connection to the service and cannot be restored.
-
OnStopped– called when the session stops attempting to restore the connection to the service, which can be useful for establishing a new connection. -
Timeout– the maximum timeout during which the session can be restored after losing connection to the service.
session, err := db.Coordination().CreateSession(ctx,
"/path/to/mynode", // name of the Coordination Node in the database
)
Before working with semaphores the client opens a session (see CoordinationSession): calling createSession creates a session object, and connect() establishes a bidirectional gRPC stream with a node. Retry parameters and connection timeout are set in CoordinationSessionSettings (withConnectTimeout, withRetryPolicy, withExecutor).
Typical scenario: after a successful connect() you perform semaphore operations, then close the session via close() (conveniently — try-with-resources). While the session is active, the SDK automatically retries the connection on network failures according to the settings.
import tech.ydb.coordination.CoordinationClient;
import tech.ydb.coordination.CoordinationSession;
import tech.ydb.coordination.settings.CoordinationSessionSettings;
import tech.ydb.core.grpc.GrpcTransport;
public class CoordinationSessionExample {
private static final String NODE_PATH_SUFFIX = "/path/to/mynode";
public static void main(String[] args) {
String connectionString = System.getenv().getOrDefault(
"YDB_CONNECTION_STRING", "grpc://localhost:2136/local");
try (GrpcTransport transport = GrpcTransport.forConnectionString(connectionString).build();
CoordinationClient client = CoordinationClient.newClient(transport).build()) {
String nodePath = client.getDatabase() + NODE_PATH_SUFFIX;
client.createNode(nodePath).join().expectSuccess("не удалось создать узел");
// try-with-resources guarantees the call to close() and stopping the thread with the node
try (CoordinationSession session = client.createSession(
nodePath,
CoordinationSessionSettings.newBuilder().build())) {
// Establish a connection to the coordination node
session.connect().join().expectSuccess("не удалось подключить сессию");
System.out.println("Сессия подключена, id=" + session.getId());
// ... semaphore operations (see the “Working with semaphores” section) ...
} // session.close() — explicit session termination
}
}
}
import ydb
client = driver.coordination_client
with client.session("/path/to/mynode") as session:
# working with the session
pass
import ydb
client = driver.coordination_client
async with client.session("/path/to/mynode") as session:
# working with the session
pass
This functionality is not currently supported.
import { CoordinationClient } from "@ydbjs/coordination";
let client = new CoordinationClient(driver);
await using session = await client.createSession("/path/to/mynode", {}, signal);
The session is created by CoordinationClient::create_session with a path to a node and SessionOptions (SessionOptionsBuilder: timeout, description, etc.). The thread with the node is started inside the session constructor; there is no separate connect call as in Java.
use ydb::SessionOptionsBuilder;
let session = coordination_client
.create_session(
"/path/to/mynode".into(),
SessionOptionsBuilder::default().build()?,
)
.await?;
This functionality is not currently supported.
Session termination control
Your client application must monitor the session state, because it can rely on the state of acquired semaphores only while the session is active. When the session ends by client or server initiative, the client can no longer be sure that other clients in the cluster have not acquired its semaphores and changed their state.
In the C++ SDK, the established session maintains and automatically restores the connection to the YDB cluster in the background.
In the Go SDK, a session context session.Context() is used to track such situations; it ends together with the session. The SDK independently handles transport-level errors and restores the connection to the service, attempting to restore the session when possible. Thus, you only need to monitor the session context to react promptly to its loss.
In the Python SDK, the session automatically restores the connection to the YDB cluster on failures. It is recommended to use a context manager (with or async with) to ensure the session is closed when exiting the block. When working with semaphores via a context manager (with session.semaphore(name) or async with session.semaphore(name)), the semaphore is automatically released upon exiting the block, and the session is closed when the context ends.
This functionality is not currently supported.
In the JS SDK, a signal session.signal is used to track such situations; it is aborted together with the session. The SDK independently handles transport-level errors and restores the connection to the service, attempting to restore the session when possible. Thus, you only need to monitor the session signal to avoid performing actions when the session has been closed or expired.
The JavaScript SDK also provides a method to obtain a new session when the old one is lost, and this approach is recommended for long-term use for await (session of client.openSession()) { session.signal }.
Close the session (close()) when your scenario has finished: this explicitly releases the connection to the node. While the session remains open, the SDK automatically retries the connection on network failures according to CoordinationSessionSettings. Hold a semaphore only for the duration of solving the user task and release it via SemaphoreLease.release() when the resource is no longer needed.
With CoordinationSession call alive: it returns CancellationToken — it is cancelled when the session ends (similar to Go's context tracking). When releasing Lease or when calling Drop on the session, the semaphore release is sent to the server in the background.
This functionality is not currently supported.
Working with semaphores
Creating a semaphore
When creating a semaphore you can specify its limit. The limit defines the maximum value to which it can be increased. Calls that try to increase the semaphore value beyond this limit will wait until their increase requests can be fulfilled so that the semaphore value does not exceed its limit.
session
.CreateSemaphore(
"my-semaphore", // semaphore name
10 // semaphore limit
)
.ExtractValueSync()
.ExtractResult();
You can also pass a string when creating a semaphore, which will be stored with the semaphore and returned when it is acquired:
session
.CreateSemaphore(
"my-semaphore", // semaphore name
10, // semaphore limit
"my-data" // semaphore data
)
.ExtractValueSync()
.ExtractResult();
err := session.CreateSemaphore(ctx,
"my-semaphore", // semaphore name
10 // semaphore limit
)
In the Python SDK the semaphore is created implicitly on the first acquire() call in the session.semaphore(name, limit) method. The limit is specified when creating the semaphore object.
import ydb
client = driver.coordination_client
with client.session("/path/to/mynode") as session:
# the semaphore will be created on the first acquire() with a limit of 10
semaphore = session.semaphore("my-semaphore", 10)
import ydb
client = driver.coordination_client
async with client.session("/path/to/mynode") as session:
# the semaphore will be created on the first acquire() with a limit of 10
semaphore = session.semaphore("my-semaphore", 10)
This functionality is not currently supported.
const sem = session.semaphore("connections");
await sem.create({
limit: 10,
data: new Uint8Array(),
});
Below is a complete example of a semaphore lifecycle semaphore: creating a node and session, creating the semaphore, acquiring, updating and reading data, and releasing. A persistent semaphore must be created explicitly (createSemaphore); ephemeral semaphores are created on first acquire (see the “Acquiring a semaphore” section).
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import tech.ydb.coordination.CoordinationClient;
import tech.ydb.coordination.CoordinationSession;
import tech.ydb.coordination.SemaphoreLease;
import tech.ydb.coordination.description.SemaphoreDescription;
import tech.ydb.coordination.settings.DescribeSemaphoreMode;
import tech.ydb.core.grpc.GrpcTransport;
public class CoordinationSemaphoreExample {
private static final String NODE_PATH_SUFFIX = "/path/to/mynode";
private static final String SEMAPHORE_NAME = "my-semaphore";
public static void main(String[] args) {
String connectionString = System.getenv().getOrDefault(
"YDB_CONNECTION_STRING", "grpc://localhost:2136/local");
try (GrpcTransport transport = GrpcTransport.forConnectionString(connectionString).build();
CoordinationClient client = CoordinationClient.newClient(transport).build()) {
String nodePath = client.getDatabase() + NODE_PATH_SUFFIX;
// 1. Create a coordination node
client.createNode(nodePath).join().expectSuccess("не удалось создать узел");
try (CoordinationSession session = client.createSession(nodePath)) {
session.connect().join().expectSuccess("не удалось подключить сессию");
byte[] initialData = "my-data".getBytes(StandardCharsets.UTF_8);
// 2. Create a semaphore with a limit of 10 and initial data
session.createSemaphore(SEMAPHORE_NAME, 10, initialData)
.join().expectSuccess("не удалось создать семафор");
// 3. Acquire 5 tokens; wait in the queue for no more than 30 seconds
SemaphoreLease lease = session
.acquireSemaphore(SEMAPHORE_NAME, 5, Duration.ofSeconds(30))
.join().getValue();
try {
// 4. Update the data attached to the semaphore
byte[] updatedData = "updated-data".getBytes(StandardCharsets.UTF_8);
session.updateSemaphore(SEMAPHORE_NAME, updatedData)
.join().expectSuccess("не удалось обновить данные семафора");
// 5. Read the current state of the semaphore
SemaphoreDescription description = session
.describeSemaphore(SEMAPHORE_NAME, DescribeSemaphoreMode.DATA_ONLY)
.join().getValue();
System.out.println("Имя: " + description.getName());
System.out.println("Лимит: " + description.getLimit());
System.out.println("Захвачено: " + description.getCount());
System.out.println("Данные: "
+ new String(description.getData(), StandardCharsets.UTF_8));
} finally {
// 6. Release the acquired tokens
lease.release().join().expectSuccess("не удалось освободить семафор");
}
}
}
}
}
If a semaphore with that name already exists, createSemaphore returns a “already exists” status. The version without the data parameter is equivalent to passing null.
CoordinationSession::create_semaphore takes a name, a limit, and arbitrary bytes data stored in the semaphore.
session.create_semaphore("my-semaphore", 10, vec![]).await?;
// or with custom data stored in the semaphore:
session
.create_semaphore("other-semaphore", 10, b"my-data".to_vec())
.await?;
This functionality is not currently supported.
Acquiring a semaphore
To acquire a semaphore, the client must call the AcquireSemaphore method and wait for the special Lease object. This object serves as confirmation that the semaphore value was successfully increased and can be considered as such until the semaphore is explicitly released or the session in which the confirmation was obtained ends.
session
.AcquireSemaphore(
"my-semaphore", // semaphore name
TAcquireSemaphoreSettings().Count(5) // value to increase semaphore by
)
.ExtractValueSync()
.ExtractResult();
When acquiring, you can optionally pass a TAcquireSemaphoreSettings structure with the following settings:
-
Count– the amount by which the semaphore is increased on acquire. -
Data– additional data that can be stored in the semaphore. -
OnAccepted– called when the operation is queued (for example, if the semaphore could not be acquired immediately).- Will not be called if the semaphore is acquired immediately.
- Note that the call may occur concurrently with the result
TFuture.
-
Timeout– the maximum time the operation may remain in the server queue.- The operation returns
falseif the semaphore could not be acquired withinTimeoutafter being queued. - When
Timeoutis set to 0, the operation effectively works likeTryAcquire, i.e., the semaphore will be acquired atomically and the operation returnstrue, or the operation returnsfalsewithout using queues.
- The operation returns
-
Ephemeral– iftrue, the name is an ephemeral semaphore; such semaphores are created automatically on the firstAcquireand automatically removed with the lastRelease. -
Shared()– alias for settingCount = 1, acquiring the semaphore in shared mode. -
Exclusive()– alias for settingCount = max, acquiring the semaphore in exclusive mode (for semaphores created with a limit ofMax<ui64>()).
lease, err := session.AcquireSemaphore(ctx,
"my-semaphore", // semaphore name
5, // value to increase semaphore by
)
To cancel waiting for a semaphore acquisition, simply cancel the ctx context passed to the method.
import ydb
client = driver.coordination_client
with client.session("/path/to/mynode") as session:
semaphore = session.semaphore("my-semaphore", 10)
with semaphore:
# semaphore acquired for 1 unit (default value)
pass
# or manually:
semaphore = session.semaphore("my-semaphore", 10)
semaphore.acquire(count=5)
# working with the resource
semaphore.release()
import ydb
client = driver.coordination_client
async with client.session("/path/to/mynode") as session:
semaphore = session.semaphore("my-semaphore", 10)
async with semaphore:
# semaphore acquired for 1 unit (default value)
pass
# or manually:
semaphore = session.semaphore("my-semaphore", 10)
await semaphore.acquire(count=5)
# working with the resource
await semaphore.release()
This functionality is not currently supported.
{
await using lease = await sem.acquire({ count: 1, data: new Uint8Array() });
await doWork(lease.signal);
} // lease.release() called automatically
Acquisition is performed via acquireSemaphore (full example – see the “Creating a semaphore” section). The method takes the semaphore name, the number of tokens count, optional operation data, and a queue wait timeout java.time.Duration. It returns CompletableFuture<Result<SemaphoreLease>> (see Result and SemaphoreLease). If a semaphore with the specified name does not exist, the operation ends with an exception.
For ephemeral semaphores, use acquireEphemeralSemaphore (the exclusive flag sets the acquisition mode); such semaphores are created on first acquire and removed after the final release.
At any given time a session can hold only one semaphore; subsequent calls for the same name replace the previous operation (for example, to decrease count or change the timeout).
acquire_semaphore returns Lease. The queue wait timeout, ephemerality, and operation data are set via AcquireOptionsBuilder and acquire_semaphore_with_params.
use std::time::Duration;
use ydb::AcquireOptionsBuilder;
let _lease = session.acquire_semaphore("my-semaphore", 5).await?;
let opts = AcquireOptionsBuilder::default()
.timeout(Duration::from_secs(30))
.build()?;
let _lease = session
.acquire_semaphore_with_params("my-semaphore", 5, opts)
.await?;
This functionality is not currently supported.
You can decrease (but not increase) the acquired semaphore's value by calling its method AcquireSemaphore again with a smaller value.
Updating semaphore data
You can update (replace) the semaphore data bound at its creation using the method UpdateSemaphore.
session
.UpdateSemaphore(
"my-semaphore", // semaphore name
"updated-data" // new semaphore data
)
.ExtractValueSync()
.ExtractResult();
err := session.UpdateSemaphore(
"my-semaphore", // semaphore name
options.WithUpdateData([]byte("updated-data")), // new semaphore data
)
import ydb
client = driver.coordination_client
with client.session("/path/to/mynode") as session:
semaphore = session.semaphore("my-semaphore", 10)
semaphore.update(b"updated-data")
import ydb
client = driver.coordination_client
async with client.session("/path/to/mynode") as session:
semaphore = session.semaphore("my-semaphore", 10)
await semaphore.update(b"updated-data")
This functionality is not currently supported.
const sem = session.semaphore("connections");
await sem.update({
limit: 5,
data: new Uint8Array(),
});
Data update — the method updateSemaphore (step 4 in the “Creating a semaphore” example). The call does not require acquiring the semaphore and does not result in it.
session
.update_semaphore("my-semaphore", b"updated-data".to_vec())
.await?;
This functionality is not currently supported.
This call does not require acquiring the semaphore and does not result in it. If you need the data to be updated by only a single client, you must ensure this explicitly, for example by acquiring the semaphore, updating the data, and releasing the semaphore back.
Getting semaphore data
session
.DescribeSemaphore(
"my-semaphore" // semaphore name
)
.ExtractValueSync()
.ExtractResult();
When retrieving semaphore information, you can optionally pass a TDescribeSemaphoreSettings structure with the following settings:
OnChanged– called once after data changes on the server. With theboolparameter, iftrue– the call occurred because of some changes; iffalse– it is a spurious call and you need to repeatDescribeSemaphoreto restore the subscription.WatchData– invokeOnChangedwhen the semaphore data changes.WatchOwners– invokeOnChangedwhen the semaphore owners change.IncludeOwners– return the list of owners in the results.IncludeWaiters– return the list of waiters in the results.
The call result is a structure with the following fields:
Name– semaphore name.Data– semaphore data.Count– current semaphore value.Limit– maximum number of tokens specified when the semaphore was created.Owners– list of semaphore owners.Waiters– list of waiters in the semaphore queue.Ephemeral– indicates whether the semaphore is ephemeral.
The Owners and Waiters fields in the result are lists of structures with the following fields:
OrderId– sequential number of the acquire operation on the semaphore. It can be used for identification, for example ifOrderIdchanged, it means the session performedReleaseSemaphoreand a newAcquireSemaphore.SessionId– identifier of the session that performed thisAcquireSemaphore.Timeout– timeout with whichAcquireSemaphorewas called for queued operations.Count– value requested inAcquireSemaphore.Data– data that were specified inAcquireSemaphore.
description, err := session.DescribeSemaphore(
"my-semaphore" // semaphore name
options.WithDescribeOwners(true), // to get list of owners
options.WithDescribeWaiters(true), // to get list of waiters
)
import ydb
client = driver.coordination_client
with client.session("/path/to/mynode") as session:
semaphore = session.semaphore("my-semaphore", 10)
description = semaphore.describe()
# description contains: name, data, count, limit, owners, waiters, ephemeral
import ydb
client = driver.coordination_client
async with client.session("/path/to/mynode") as session:
semaphore = session.semaphore("my-semaphore", 10)
description = await semaphore.describe()
# description contains: name, data, count, limit, owners, waiters, ephemeral
This functionality is not currently supported.
const sem = session.semaphore("connections");
await sem.describe({
owners: true,
waiters: true,
});
Reading semaphore state — the method describeSemaphore (step 5 in the “Creating a semaphore” example). It takes the semaphore name and a mode DescribeSemaphoreMode: data only, with owners list, with waiters list, or both lists.
Elements of the owners and waiters lists (getOwnersList, getWaitersList) provide the session identifier, timeout, requested count, operation data, and orderId (see the nested type SemaphoreDescription.Session in the source).
To subscribe to changes, use watchSemaphore with the same description mode and WatchSemaphoreMode (data, owners, or both). The SemaphoreWatcher object contains a snapshot of SemaphoreDescription and getChangedFuture() — CompletableFuture<Result<SemaphoreChangedEvent>> (see SemaphoreChangedEvent, fields isDataChanged, isOwnersChanged). The Future completes on the next event; after notification, call watchSemaphore again to continue watching (see tests).
describe_semaphore by default requests owners and waiters. You can set the flag set via DescribeOptions and describe_semaphore_with_params. To subscribe to changes, see WatchOptions in the crate documentation.
let description = session.describe_semaphore("my-semaphore").await?;
This functionality is not currently supported.
Releasing a semaphore
session
.ReleaseSemaphore(
"my-semaphore" // semaphore name
)
.ExtractValueSync()
.ExtractResult();
To release a semaphore acquired in a session, you need to call the Release method on the Lease object.
err := lease.Release()
In the Python SDK, the semaphore is released by the release() method on the semaphore object. When using a context manager (with or async with), the release happens automatically when exiting the block.
import ydb
client = driver.coordination_client
with client.session("/path/to/mynode") as session:
semaphore = session.semaphore("my-semaphore", 10)
semaphore.acquire(count=5)
# working with the resource
semaphore.release()
import ydb
client = driver.coordination_client
async with client.session("/path/to/mynode") as session:
semaphore = session.semaphore("my-semaphore", 10)
await semaphore.acquire(count=5)
# working with the resource
await semaphore.release()
This functionality is not currently supported.
To release a semaphore acquired in a session, you need to call the Release method on the Lease object. If the semaphore was taken using a using construct, it will be released automatically when exiting the scope.
await lease.release();
Release is performed via SemaphoreLease.release() (step 6 in the “Creating a semaphore” example). The method is asynchronous and returns CompletableFuture<Status>.
Call Lease::release or simply relinquish ownership of Lease — when the value is dropped, a release is also sent to the server.
let lease = session.acquire_semaphore("my-semaphore", 1).await?;
// …
lease.release();
This functionality is not currently supported.
Important features
Operations AcquireSemaphore and ReleaseSemaphore are idempotent. If AcquireSemaphore was called on the semaphore, subsequent calls to AcquireSemaphore only change the acquisition parameters. For example, calling AcquireSemaphore with count=10 may add an operation to the queue. Before or after a successful acquisition you can call AcquireSemaphore again with count=9, decreasing the number of acquired units; the new operation will replace the old one (which will finish with code ABORTED if it has not yet completed successfully). The position in the queue does not change, despite replacing one operation AcquireSemaphore with another.
The AcquireSemaphore and ReleaseSemaphore operations return bool indicating whether the operation changed the semaphore state. For example, AcquireSemaphore returns false if acquiring the semaphore fails within Timeout because it was held by another. The ReleaseSemaphore operation may return false if the semaphore is not held in the current session.
You can complete the queued operation AcquireSemaphore early by calling ReleaseSemaphore. Regardless of the number of AcquireSemaphore calls for a particular semaphore in a single session, release occurs with a single ReleaseSemaphore call, i.e., operations AcquireSemaphore and ReleaseSemaphore cannot be used as equivalents of Acquire or Release on a recursive mutex.
The DescribeSemaphore operation with flags WatchData or WatchOwners creates a subscription to semaphore changes. Any older subscription to the same semaphore in the session is cancelled, triggering OnChanged(false). It is recommended to ignore OnChanged from previous DescribeSemaphore calls if a new overriding call is made, for example by remembering the current call id.
The OnChanged(false) call can occur not only due to cancellation by a new DescribeSemaphore, but also for other reasons, such as a temporary connection break between the gRPC client and server, a temporary break between the gRPC server and the current service leader, or a change of the service leader—that is, at the slightest suspicion that a notification may have been lost. To restore the subscription, client code should make a new DescribeSemaphore call, correctly handling the possibility that the result of the new call may differ (for example, if the notification was indeed lost).