refactor (backend-rs): never throw an error on ID generation

This commit is contained in:
naskya 2024-04-24 07:37:16 +09:00
parent 9db729d734
commit 879d499486
No known key found for this signature in database
GPG key ID: 712D413B3A9FED5C
5 changed files with 71 additions and 55 deletions

View file

@ -1129,8 +1129,6 @@ export enum ChatEvent {
Typing = 'typing' Typing = 'typing'
} }
export function publishToChatStream(senderUserId: string, receiverUserId: string, kind: ChatEvent, object: any): void export function publishToChatStream(senderUserId: string, receiverUserId: string, kind: ChatEvent, object: any): void
/** Initializes Cuid2 generator. Must be called before any [create_id]. */
export function initIdGenerator(length: number, fingerprint: string): void
export function getTimestamp(id: string): number export function getTimestamp(id: string): number
/** /**
* The generated ID results in the form of `[8 chars timestamp] + [cuid2]`. * The generated ID results in the form of `[8 chars timestamp] + [cuid2]`.

View file

@ -310,7 +310,7 @@ if (!nativeBinding) {
throw new Error(`Failed to load native binding`) throw new Error(`Failed to load native binding`)
} }
const { loadEnv, loadConfig, stringToAcct, acctToString, addNoteToAntenna, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getNoteSummary, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, ChatEvent, publishToChatStream, initIdGenerator, getTimestamp, genId, genIdAt, secureRndstr } = nativeBinding const { loadEnv, loadConfig, stringToAcct, acctToString, addNoteToAntenna, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getNoteSummary, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, ChatEvent, publishToChatStream, getTimestamp, genId, genIdAt, secureRndstr } = nativeBinding
module.exports.loadEnv = loadEnv module.exports.loadEnv = loadEnv
module.exports.loadConfig = loadConfig module.exports.loadConfig = loadConfig
@ -356,7 +356,6 @@ module.exports.UserProfileFfvisibilityEnum = UserProfileFfvisibilityEnum
module.exports.UserProfileMutingnotificationtypesEnum = UserProfileMutingnotificationtypesEnum module.exports.UserProfileMutingnotificationtypesEnum = UserProfileMutingnotificationtypesEnum
module.exports.ChatEvent = ChatEvent module.exports.ChatEvent = ChatEvent
module.exports.publishToChatStream = publishToChatStream module.exports.publishToChatStream = publishToChatStream
module.exports.initIdGenerator = initIdGenerator
module.exports.getTimestamp = getTimestamp module.exports.getTimestamp = getTimestamp
module.exports.genId = genId module.exports.genId = genId
module.exports.genIdAt = genIdAt module.exports.genIdAt = genIdAt

View file

@ -1,21 +1,31 @@
use crate::database::{redis_conn, redis_key}; use crate::database::{redis_conn, redis_key};
use crate::model::entity::note; use crate::model::entity::note;
use crate::service::stream; use crate::service::stream;
use crate::util::id::get_timestamp; use crate::util::id::{get_timestamp, InvalidIdErr};
use redis::{streams::StreamMaxlen, Commands}; use redis::{streams::StreamMaxlen, Commands, RedisError};
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Redis error: {0}")]
RedisErr(#[from] RedisError),
#[error("Invalid ID: {0}")]
InvalidIdErr(#[from] InvalidIdErr),
#[error("Stream error: {0}")]
StreamErr(#[from] stream::Error),
}
type Note = note::Model; type Note = note::Model;
#[crate::export] #[crate::export]
pub fn add_note_to_antenna(antenna_id: String, note: &Note) -> Result<(), stream::Error> { pub fn add_note_to_antenna(antenna_id: String, note: &Note) -> Result<(), Error> {
// for timeline API // for timeline API
redis_conn()?.xadd_maxlen( redis_conn()?.xadd_maxlen(
redis_key(format!("antennaTimeline:{}", antenna_id)), redis_key(format!("antennaTimeline:{}", antenna_id)),
StreamMaxlen::Approx(200), StreamMaxlen::Approx(200),
format!("{}-*", get_timestamp(&note.id)), format!("{}-*", get_timestamp(&note.id)?),
&[("note", &note.id)], &[("note", &note.id)],
)?; )?;
// for streaming API // for streaming API
stream::antenna::publish(antenna_id, note) Ok(stream::antenna::publish(antenna_id, note)?)
} }

View file

@ -1,54 +1,63 @@
//! ID generation utility based on [cuid2] //! ID generation utility based on [cuid2]
use crate::config::CONFIG;
use basen::BASE36; use basen::BASE36;
use chrono::{DateTime, NaiveDateTime, Utc}; use chrono::{DateTime, NaiveDateTime, Utc};
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use std::cmp; use std::cmp;
#[derive(thiserror::Error, Debug, PartialEq, Eq)]
#[error("ID generator has not been initialized yet")]
pub struct ErrorUninitialized;
static FINGERPRINT: OnceCell<String> = OnceCell::new(); static FINGERPRINT: OnceCell<String> = OnceCell::new();
static GENERATOR: OnceCell<cuid2::CuidConstructor> = OnceCell::new(); static GENERATOR: OnceCell<cuid2::CuidConstructor> = OnceCell::new();
const TIME_2000: i64 = 946_684_800_000; const TIME_2000: i64 = 946_684_800_000;
const TIMESTAMP_LENGTH: u16 = 8; const TIMESTAMP_LENGTH: u8 = 8;
/// Initializes Cuid2 generator. Must be called before any [create_id]. /// Initializes Cuid2 generator.
#[crate::export] fn init_id_generator(length: u8, fingerprint: &str) {
pub fn init_id_generator(length: u16, fingerprint: &str) {
FINGERPRINT.get_or_init(move || format!("{}{}", fingerprint, cuid2::create_id())); FINGERPRINT.get_or_init(move || format!("{}{}", fingerprint, cuid2::create_id()));
GENERATOR.get_or_init(move || { GENERATOR.get_or_init(move || {
cuid2::CuidConstructor::new() cuid2::CuidConstructor::new()
// length to pass shoule be greater than or equal to 8. // length to pass shoule be greater than or equal to 8.
.with_length(cmp::max(length - TIMESTAMP_LENGTH, 8)) .with_length(cmp::max(length - TIMESTAMP_LENGTH, 8).into())
.with_fingerprinter(|| FINGERPRINT.get().unwrap().clone()) .with_fingerprinter(|| FINGERPRINT.get().unwrap().clone())
}); });
} }
/// Returns Cuid2 with the length specified by [init_id]. Must be called after /// Returns Cuid2 with the length specified by [init_id_generator].
/// [init_id], otherwise returns [ErrorUninitialized]. /// It automatically calls [init_id_generator], if the generator has not been initialized.
pub fn create_id(datetime: &NaiveDateTime) -> Result<String, ErrorUninitialized> { fn create_id(datetime: &NaiveDateTime) -> String {
match GENERATOR.get() { if GENERATOR.get().is_none() {
None => Err(ErrorUninitialized), let length = match &CONFIG.cuid {
Some(gen) => { Some(cuid) => cmp::min(cmp::max(cuid.length.unwrap_or(16), 16), 24),
let date_num = cmp::max(0, datetime.and_utc().timestamp_millis() - TIME_2000) as u64; None => 16,
Ok(format!( };
"{:0>8}{}", let fingerprint = match &CONFIG.cuid {
BASE36.encode_var_len(&date_num), Some(cuid) => cuid.fingerprint.as_deref().unwrap_or_default(),
gen.create_id() None => "",
)) };
} init_id_generator(length, fingerprint);
} }
let date_num = cmp::max(0, datetime.and_utc().timestamp_millis() - TIME_2000) as u64;
format!(
"{:0>8}{}",
BASE36.encode_var_len(&date_num),
GENERATOR.get().unwrap().create_id()
)
}
#[derive(thiserror::Error, Debug)]
#[error("Invalid ID: {id}")]
pub struct InvalidIdErr {
id: String,
} }
#[crate::export] #[crate::export]
pub fn get_timestamp(id: &str) -> i64 { pub fn get_timestamp(id: &str) -> Result<i64, InvalidIdErr> {
let n: Option<u64> = BASE36.decode_var_len(&id[0..8]); let n: Option<u64> = BASE36.decode_var_len(&id[0..8]);
match n { if let Some(n) = n {
None => -1, Ok(n as i64 + TIME_2000)
Some(n) => n as i64 + TIME_2000, } else {
Err(InvalidIdErr { id: id.to_string() })
} }
} }
@ -60,35 +69,41 @@ pub fn get_timestamp(id: &str) -> i64 {
/// Ref: https://github.com/paralleldrive/cuid2#parameterized-length /// Ref: https://github.com/paralleldrive/cuid2#parameterized-length
#[crate::export] #[crate::export]
pub fn gen_id() -> String { pub fn gen_id() -> String {
create_id(&Utc::now().naive_utc()).unwrap() create_id(&Utc::now().naive_utc())
} }
/// Generate an ID using a specific datetime /// Generate an ID using a specific datetime
#[crate::export] #[crate::export]
pub fn gen_id_at(date: DateTime<Utc>) -> String { pub fn gen_id_at(date: DateTime<Utc>) -> String {
create_id(&date.naive_utc()).unwrap() create_id(&date.naive_utc())
} }
#[cfg(test)] #[cfg(test)]
mod unit_test { mod unit_test {
use crate::util::id; use super::{gen_id, gen_id_at, get_timestamp};
use chrono::Utc; use chrono::{Duration, Utc};
use pretty_assertions::{assert_eq, assert_ne}; use pretty_assertions::{assert_eq, assert_ne};
use std::thread; use std::thread;
#[test] #[test]
fn can_create_and_decode_id() { fn can_create_and_decode_id() {
let now = Utc::now().naive_utc(); let now = Utc::now();
assert_eq!(id::create_id(&now), Err(id::ErrorUninitialized)); assert_eq!(gen_id().len(), 16);
id::init_id_generator(16, ""); assert_ne!(gen_id_at(now), gen_id_at(now));
assert_eq!(id::create_id(&now).unwrap().len(), 16); assert_ne!(gen_id(), gen_id());
assert_ne!(id::create_id(&now).unwrap(), id::create_id(&now).unwrap());
let id1 = thread::spawn(move || id::create_id(&now).unwrap()); let id1 = thread::spawn(move || gen_id_at(now));
let id2 = thread::spawn(move || id::create_id(&now).unwrap()); let id2 = thread::spawn(move || gen_id_at(now));
assert_ne!(id1.join().unwrap(), id2.join().unwrap()); assert_ne!(id1.join().unwrap(), id2.join().unwrap());
let test_id = id::create_id(&now).unwrap(); let test_id = gen_id_at(now);
let timestamp = id::get_timestamp(&test_id); let timestamp = get_timestamp(&test_id).unwrap();
assert_eq!(now.and_utc().timestamp_millis(), timestamp); assert_eq!(now.timestamp_millis(), timestamp);
let now_id = gen_id_at(now);
let old_id = gen_id_at(now - Duration::milliseconds(1));
let future_id = gen_id_at(now + Duration::milliseconds(1));
assert!(old_id < now_id);
assert!(now_id < future_id);
} }
} }

View file

@ -1,17 +1,11 @@
import cluster from "node:cluster"; import cluster from "node:cluster";
import { config } from "@/config.js";
import { initDb } from "@/db/postgre.js"; import { initDb } from "@/db/postgre.js";
import { initIdGenerator } from "backend-rs";
import os from "node:os"; import os from "node:os";
/** /**
* Init worker process * Init worker process
*/ */
export async function workerMain() { export async function workerMain() {
const length = Math.min(Math.max(config.cuid?.length ?? 16, 16), 24);
const fingerprint = config.cuid?.fingerprint ?? "";
initIdGenerator(length, fingerprint);
await initDb(); await initDb();
if (!process.env.mode || process.env.mode === "web") { if (!process.env.mode || process.env.mode === "web") {