Write Data
Use the TypeScript client to write to a Synnax cluster.
The Synnax TypeScript client supports multiple methods for writing data to a cluster. We can write directly to a channel, or we can write to multiple channels in a streaming fashion using a writer.
Writes in Synnax are more complicated than reads, and, as such, we recommend checking out our concepts page to learn more about writing data to Synnax. The rules of writes are especially important.
Writing to a Channel
Writing to a channel requires us to write timestamps to its index channel before we write the data. We’ll create the following channels to use as examples:
import { DataType } from "@synnaxlabs/client";
const timeChannel = await client.channels.create({
name: "time",
dataType: DataType.TIMESTAMP,
isIndex: true,
});
const temperatureChannel = await client.channels.create({
name: "temperature",
dataType: DataType.FLOAT32,
index: timeChannel.key,
});
We can then write data to timeChannel
and temperatureChannel
. We need to write to
the index channel before writing to the regular channel, so we will write to
timeChannel
first. Writing data via the channel’s write
method requires a typed
array:
import { TimeStamp, TimeSpan } from "@synnaxlabs/client";
const start = TimeStamp.now();
const timestamps = new BigInt64Array([
start.valueOf(), // valueOf() converts a TimeSpan to a bigint.
start.add(TimeSpan.seconds(1)).valueOf(),
start.add(TimeSpan.seconds(2)).valueOf(),
start.add(TimeSpan.seconds(3)).valueOf(),
start.add(TimeSpan.seconds(4)).valueOf(),
]);
const temperatures = new Float32Array([20.0, 20.1, 20.2, 20.3, 20.4]);
// Write the timestamps to the index first
await timeChannel.write(start, timestamps);
// Then write the data
await temperatureChannel.write(start, temperatures);
Notice how we align the two arrays by using the common start
timestamp. This
tells Synnax that the first sample in the temperatures
array is associated
with the first timestamp in the timestamps
array, and so on.
Synnax will raise a ValidationError
if the index does not contain a
corresponding timestamp for every sample in the data array. After all, it
wouldn’t make sense to have a temperature reading without an associated
timestamp.
Using a Writer
While the above methods are great for writing static, existing data, it’s common
to want to write data in a streaming fashion as it’s acquired. This is useful for
for use in control sequences and live data processing. The Writer
class is designed
to handle this use case (and is actually used under the hood by the above methods).
To keep things intuitive, we’ve designed the writer API around a file-like interface. There are a few key differences, the most important being that writers are governed by a transaction. If you’d like to learn more about transactions, see the concepts page.
We’ll create the following channels to use as examples:
import { DataType } from "@synnaxlabs/client";
const timeChannel = await client.channels.create({
name: "time",
dataType: DataType.TIMESTAMP,
isIndex: true,
});
const temperatureChannel = await client.channels.create({
name: "temperature",
dataType: DataType.FLOAT32,
index: timeChannel.key,
});
To open the writer, we’ll use the openWriter
method on the client:
import { TimeStamp, Series, Frame } from "@synnaxlabs/client";
const writer = await client.openWriter({
start: TimeStamp.now(),
channels: ["time", "temperature"]
});
try {
for (let i = 0; i < 100; i++) {
const start = TimeStamp.now();
const timeSeries = BigInt64Array.from({ length: 10 }, (_, i) => (
start.add(TimeSpan.milliseconds(i)).valueOf()
))
const dataSeries = Float32Array.from({ length: 10 }, (_, i) => (
Math.sin(i / 100)
))
await writer.write(new Frame({
[timeChannel.key]: new Series(timeSeries),
[temperatureChannel.key]: new Series(dataSeries)
}));
await new Promise(resolve => setTimeout(resolve, 100));
}
await writer.commit():
} finally {
await writer.close()
}
This example will write 100 batches of 10 samples to the temperature
channel, each
roughly 100ms apart, and will commit all writes when finished.
It’s typical to write and commit millions of samples over the course of hours or days, intermittently calling commit to ensure that the data is safely stored in the cluster.
Closing the Writer
It’s very important to free the writer resources when finished by calling the
close
method. If close
is not called at the end of the writer, other writers
may not be able to write to the same channels. We typically recommend placing
the writer operations inside a try-finally block to ensure that the writer is
always closed.
Different Ways of Writing Data
There are a number of argument formats that the write
method accepts. Use the
one that best fits your use case.
// Write a single sample for a channel
await writer.write("temperature", 20.0);
// Write multiple samples for a channel
await writer.write("temperature", [20.0, 20.1, 20.2, 20.3, 20.4]);
// Write a single sample for several channels
await writer.write({
time: TimeStamp.now(),
temperature: 20.0,
});
// Write multiple samples for several channels
const start = TimeStamp.now();
await writer.write({
time: [start, start.add(TimeSpan.seconds(1))],
temperature: [20.0, 20.1],
});
// Write typed arrays for several channels
await writer.write(
new Frame({
[timeChannel.key]: new BigInt64Array([
start.valueOf(),
start.add(TimeSpan.seconds(1)).valueOf(),
]),
[temperatureChannel.key]: new Float32Array([20.0, 20.1]),
}),
);
Auto-Commit
You can configure a writer to automatically commit written data after each write,
making it immediately available for read access. To do this, set the enableAutoCommit
option to true
when opening the writer:
import { TimeStamp, Series, Frame } from "@synnaxlabs/client";
const writer = await client.openWriter({
start: TimeStamp.now(),
channels: ["time", "temperature"],
enableAutoCommit: true,
});
try {
for (let i = 0; i < 100; i++) {
const start = TimeStamp.now();
await writer.write(
new Frame({
time: start,
temperature: Math.sin(i / 100),
}),
);
await new Promise((resolve) => setTimeout(resolve, 100));
}
} finally {
await writer.close();
}
Write Authorities
Writers support dynamic control handoff. Multiple writers can be opened on a channel at the same time, but only one writer is allowed to write to the channel. To determine which writer has control, an authority from 0 to 255 is assigned to each writer (or, optionally, each channel in the writer). The writer with the highest authority will be allowed to write. If two writers have the same authority, the writer that opened first will be allowed to write. For more information, see the concepts page on writers.
By default, writers are opened with an authority of ABSOLUTE
i.e. 255. This means that
no other writers can write to the channel as long as the writer is open.
Opening a writer with the same authority on all channels
To open a writer with the same authority on all channels, you can pass the authority
argument with an integer.
import { TimeStamp, Series, Frame } from "@synnaxlabs/client";
const writer = await client.openWriter({
start: TimeStamp.now(),
channels: ["time", "temperature"],
authority: 100,
});
Opening a writer with different authorities on each channel
To open a writer with different authorities on each channel, you can pass the authority
argument with a list of integers. This list must be the same length as the number of channels
in the writer.
import { TimeStamp, Series, Frame } from "@synnaxlabs/client";
const writer = await client.openWriter({
start: TimeStamp.now(),
channels: ["time", "temperature"],
authority: [100, 200],
});
Adjusting write authorities after open
To change the authority of a writer during operation, you can use the setAuthority
method:
// Set the authority on all channels
await writer.setAuthority(200);
// Set the authority on just a few channels
await writer.setAuthority({
time: 200,
temperature: 100,
});
Persistence/Streaming Mode
By default, writers are opened in stream + persist
mode. To change the mode of a
writer, specify the value of the mode
argument when opening the writer. This can be
persist
, stream
, or persistStream
.
For example, to open a writer that only persists data:
import { TimeStamp, Series, Frame, WriterMode } from "@synnaxlabs/client";
const writer = await client.openWriter({
start: TimeStamp.now(),
channels: ["time", "temperature"],
mode: "persist",
});
Common Pitfalls
There are several common pitfalls to avoid when writing data to a Synnax cluster. These are important to avoid as they can lead to performance degradation and/or control issues.
Using Many Individual Write Calls Instead of a Writer
When writing large volumes of data in a streaming fashion (or in batches), it’s important
to use a writer instead of making individual write calls to a channel. Calls to write
on a channel use an entirely new transaction for each call - constantly creating, committing,
and closing transactions has a dramatic impact on performance. So, don’t do this:
time = await client.channels.retrieve("timestamps")
my_tc = await client.channels.retrieve("my_precise_tc")
# This is a very bad idea
for i in range(100):
ts = TimeStamp.now()
await time.write(ts, ts)
await my_tc.write(ts, i)
This is also a bad idea:
for (let i = 0; i < 100; i++) {
const writer = await client.openWriter({
start: TimeStamp.now(),
channels: ["time", "temperature"],
enableAutoCommit: true,
});
await writer.write({
time: TimeStamp.now(),
temperature: Math.sin(i / 100),
});
await writer.close();
}
Instead, repeatedly call write
on a single writer:
# This is dramatically more efficient
const writer = await client.openWriter({
start: TimeStamp.now(),
channels: ["time", "temperature"],
enableAutoCommit: true
});
try {
for (let i = 0; i < 100; i++)
await writer.write({
"time": TimeStamp.now(),
"temperature": Math.sin(i / 100)
});
} finally {
await writer.close();
}
Calling Commit on Every Write
If you’re not using auto-commit, it’s important to call commit
on the writer
periodically to ensure that the data is persisted to the cluster. However, calling
commit
on every write is a bad idea. This is because commit
requires a round-trip to
the cluster to ensure that the data is persisted. This can be very slow if you’re
writing a lot of data. If you’re writing a lot of data, commit every few seconds or turn
on auto-commit.
This is a bad idea:
const writer = await client.openWriter({
start: TimeStamp.now(),
channels: ["time", "temperature"],
});
try {
for (let i = 0; i < 100; i++) {
await writer.write({
time: TimeStamp.now(),
temperature: Math.sin(i / 100),
});
await writer.commit();
}
} finally {
await writer.close();
}
Instead, use auto-commit:
const writer = await client.openWriter({
start: TimeStamp.now(),
channels: ["time", "temperature"],
enableAutoCommit: true,
});
try {
for (let i = 0; i < 100; i++)
await writer.write({
time: TimeStamp.now(),
temperature: Math.sin(i / 100),
});
} finally {
await writer.close();
}