use std::io::Write;
use serde::Serialize;
use ytsaurus_yson::{YsonFormat, ser::Serializer};
use crate::error::{JobError, Result};
const TABLE_BUFFER_BYTES: usize = 256 * 1024;
#[must_use]
pub fn table_descriptor(index: usize) -> i32 {
(3 * index + 1) as i32
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TableId(usize);
impl TableId {
#[must_use]
pub fn index(self) -> usize {
self.0
}
}
impl From<usize> for TableId {
fn from(index: usize) -> Self {
TableId(index)
}
}
impl std::fmt::Display for TableId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Routing {
Descriptors,
TableSwitches { current: i64 },
}
pub struct JobWriter {
tables: Vec<Box<dyn Write>>,
logical_tables: usize,
format: YsonFormat,
routing: Routing,
scratch: Vec<u8>,
finished: bool,
names: Vec<String>,
}
impl std::fmt::Debug for JobWriter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("JobWriter")
.field("tables", &self.tables.len())
.field("format", &self.format)
.field("routing", &self.routing)
.finish_non_exhaustive()
}
}
impl JobWriter {
#[cfg(unix)]
pub fn descriptors(table_count: usize) -> Result<Self> {
let tables = (0..table_count)
.map(|i| -> Box<dyn Write> {
Box::new(std::io::BufWriter::with_capacity(
TABLE_BUFFER_BYTES,
output_descriptor(table_descriptor(i)),
))
})
.collect();
Ok(Self::from_writers(tables, YsonFormat::Binary))
}
#[cfg(unix)]
pub fn named<const N: usize>(names: [&str; N]) -> Result<(Self, [TableId; N])> {
let mut writer = Self::descriptors(N)?;
writer.names = names.iter().map(|n| (*n).to_owned()).collect();
Ok((writer, Self::ids()))
}
#[must_use]
pub fn named_writers<const N: usize>(
names: [&str; N],
tables: Vec<Box<dyn Write>>,
format: YsonFormat,
) -> (Self, [TableId; N]) {
let mut writer = Self::from_writers(tables, format);
writer.names = names.iter().map(|n| (*n).to_owned()).collect();
(writer, Self::ids())
}
fn ids<const N: usize>() -> [TableId; N] {
let mut ids = [TableId(0); N];
for (i, id) in ids.iter_mut().enumerate() {
*id = TableId(i);
}
ids
}
#[must_use]
pub fn table_name(&self, table: impl Into<TableId>) -> Option<&str> {
self.names.get(table.into().index()).map(String::as_str)
}
#[cfg(unix)]
pub fn table_switches(table_count: usize) -> Result<Self> {
let single: Box<dyn Write> = Box::new(std::io::BufWriter::with_capacity(
TABLE_BUFFER_BYTES,
output_descriptor(table_descriptor(0)),
));
Ok(Self::from_writer_with_switches(
single,
table_count,
YsonFormat::Binary,
))
}
#[must_use]
pub fn from_writer_with_switches(
sink: Box<dyn Write>,
table_count: usize,
format: YsonFormat,
) -> Self {
let mut writer = Self::from_writers(vec![sink], format);
writer.routing = Routing::TableSwitches { current: 0 };
writer.logical_tables = table_count;
writer
}
#[must_use]
pub fn from_writers(tables: Vec<Box<dyn Write>>, format: YsonFormat) -> Self {
let logical_tables = tables.len();
Self {
tables,
format,
routing: Routing::Descriptors,
scratch: Vec::with_capacity(8192),
finished: false,
logical_tables,
names: Vec::new(),
}
}
#[must_use]
pub fn table_count(&self) -> usize {
self.logical_tables
}
pub fn write<T: Serialize + ?Sized>(
&mut self,
table: impl Into<TableId>,
row: &T,
) -> Result<()> {
let table = table.into().index();
self.check_table(table)?;
let mut scratch = std::mem::take(&mut self.scratch);
scratch.clear();
let mut ser = Serializer::with_buffer(scratch, matches!(self.format, YsonFormat::Binary));
let outcome = row.serialize(&mut ser);
let encoded = ser.into_output();
let result = match outcome {
Ok(()) => self.write_bytes(table, &encoded),
Err(source) => Err(JobError::Serialize { table, source }),
};
self.scratch = encoded;
result
}
pub fn write_raw(&mut self, table: impl Into<TableId>, row: &[u8]) -> Result<()> {
let table = table.into().index();
self.check_table(table)?;
self.write_bytes(table, row)
}
fn check_table(&self, table: usize) -> Result<()> {
if table >= self.logical_tables {
return Err(JobError::UnknownTable {
index: table,
count: self.logical_tables,
names: self.names.clone(),
});
}
Ok(())
}
fn write_bytes(&mut self, table: usize, encoded: &[u8]) -> Result<()> {
let (sink_index, switch) = match &mut self.routing {
Routing::Descriptors => (table, None),
Routing::TableSwitches { current } => {
let needed = i64::try_from(table).unwrap_or(i64::MAX);
let switch = if *current == needed {
None
} else {
*current = needed;
Some(needed)
};
(0, switch)
}
};
let format = self.format;
let sink = &mut self.tables[sink_index];
if let Some(index) = switch {
let record = encode_table_switch(index, format);
sink.write_all(&record)
.map_err(|source| JobError::Write { table, source })?;
}
sink.write_all(encoded)
.and_then(|()| sink.write_all(b";"))
.map_err(|source| JobError::Write { table, source })
}
pub fn flush(&mut self) -> Result<()> {
for (table, sink) in self.tables.iter_mut().enumerate() {
sink.flush()
.map_err(|source| JobError::Write { table, source })?;
}
Ok(())
}
pub fn finish(&mut self) -> Result<()> {
self.flush()?;
self.finished = true;
Ok(())
}
}
impl Drop for JobWriter {
fn drop(&mut self) {
if self.finished {
return;
}
if let Err(e) = self.flush() {
eprintln!("ytsaurus-job: output was not flushed cleanly: {e}");
}
}
}
fn encode_table_switch(index: i64, format: YsonFormat) -> Vec<u8> {
match format {
YsonFormat::Text => format!("<table_index={index}>#;").into_bytes(),
YsonFormat::Binary => {
let mut out = vec![b'<', 0x01];
write_zigzag(b"table_index".len() as i64, &mut out);
out.extend_from_slice(b"table_index");
out.push(b'=');
out.push(0x02);
write_zigzag(index, &mut out);
out.push(b'>');
out.push(b'#');
out.push(b';');
out
}
}
}
fn write_zigzag(value: i64, out: &mut Vec<u8>) {
let mut v = ((value << 1) ^ (value >> 63)) as u64;
while v >= 0x80 {
out.push((v as u8) | 0x80);
v >>= 7;
}
out.push(v as u8);
}
#[cfg(unix)]
fn output_descriptor(fd: i32) -> OutputDescriptor {
use std::os::fd::FromRawFd;
let file = unsafe { std::fs::File::from_raw_fd(fd) };
OutputDescriptor {
file: std::mem::ManuallyDrop::new(file),
}
}
#[cfg(unix)]
#[derive(Debug)]
pub struct OutputDescriptor {
file: std::mem::ManuallyDrop<std::fs::File>,
}
#[cfg(unix)]
impl Write for OutputDescriptor {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.file.write(buf)
}
fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> {
self.file.write_all(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
self.file.flush()
}
}