Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 200 additions & 0 deletions src/ascii.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
use std::cell::Cell;
use std::os::raw::{c_char, c_int, c_void};
use std::ptr;

use crate::guard;
use crate::memcached::ffi::{AUTHZ_NONE, EXTENSION_ASCII_PROTOCOL_DESCRIPTOR, token_t};

const COMMAND: &[u8] = b"vop";
const NOT_SUPPORTED: &[u8] = b"NOT_SUPPORTED\r\n";

thread_local! {
/// Lets the `execute` the core calls right after a panicked `accept` close the connection.
static ACCEPT_PANICKED: Cell<bool> = const { Cell::new(false) };
}

type ResponseHandler =
Option<unsafe extern "C" fn(cookie: *const c_void, nbytes: c_int, dta: *const c_char) -> bool>;

pub(crate) static mut DESCRIPTOR: EXTENSION_ASCII_PROTOCOL_DESCRIPTOR =
EXTENSION_ASCII_PROTOCOL_DESCRIPTOR {
get_name: Some(get_name),
get_auth_flag: Some(get_auth_flag),
accept: Some(accept),
execute: Some(execute),
abort: Some(abort),
cookie: ptr::null(),
next: ptr::null_mut(),
};

extern "C" fn get_name(_cmd_cookie: *const c_void) -> *const c_char {
c"arcus_vector".as_ptr()
}

extern "C" fn get_auth_flag() -> u16 {
AUTHZ_NONE as u16
}

/// # Safety
///
/// Non-null `argv` must be a token with `length` readable bytes; non-null `ndata` must be writable.
unsafe extern "C" fn accept(
Comment thread
ing-eoking marked this conversation as resolved.
_cmd_cookie: *const c_void,
_cookie: *mut c_void,
argc: c_int,
argv: *mut token_t,
ndata: *mut usize,
_ptr: *mut *mut c_char,
) -> bool {
// Clear a flag whose `execute` never ran (refused by SASL), or it closes another connection.
ACCEPT_PANICKED.set(false);
let claim_and_close = || {
if !ndata.is_null() {
// SAFETY: `ndata` is non-null, so the caller guarantees it is valid to write.
unsafe { *ndata = 0 };
}
ACCEPT_PANICKED.set(true);
true
};
guard(claim_and_close, || {
if argc < 1 || argv.is_null() {
return false;
}
// SAFETY: `argc >= 1` and `argv` is non-null, so it points to a valid token.
let first = unsafe { &*argv };
if first.value.is_null() {
return false;
}
// SAFETY: `value` is non-null, so it points to `length` readable bytes.
let name = unsafe { std::slice::from_raw_parts(first.value.cast::<u8>(), first.length) };
name == COMMAND
})
}

/// # Safety
///
/// `response_handler`, if present, must accept `cookie` and bytes that live for the call.
unsafe extern "C" fn execute(
_cmd_cookie: *const c_void,
cookie: *const c_void,
_argc: c_int,
_argv: *mut token_t,
response_handler: ResponseHandler,
) -> bool {
guard(
|| false,
|| {
if ACCEPT_PANICKED.replace(false) {
return false;
}
let Some(respond) = response_handler else {
return false;
};
// SAFETY: `respond` accepts `cookie`, and `NOT_SUPPORTED` is `'static`.
unsafe {
respond(
cookie,
NOT_SUPPORTED.len() as c_int,
NOT_SUPPORTED.as_ptr().cast(),
)
}
},
)
}

extern "C" fn abort(_cmd_cookie: *const c_void, _cookie: *const c_void) {}

#[cfg(test)]
mod tests {
use super::*;

fn token(text: &str) -> token_t {
token_t {
value: text.as_ptr().cast_mut().cast(),
length: text.len(),
}
}

fn accepts(line: &[&str]) -> bool {
let mut tokens: Vec<token_t> = line.iter().map(|t| token(t)).collect();
// SAFETY: `tokens` point into `line`, which outlives the call.
unsafe {
accept(
ptr::null(),
ptr::null_mut(),
line.len() as c_int,
tokens.as_mut_ptr(),
ptr::null_mut(),
ptr::null_mut(),
)
}
}

#[test]
fn a_panicked_accept_closes_the_connection() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACCEPT_PANICKED๊ฐ€ true์ผ ๋•Œ false๋กœ ์ข…๋ฃŒํ•˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” ํ…Œ์ŠคํŠธ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค.
๊ทธ๋Ÿฐ๋ฐ response_handler๊ฐ€ None์ด์–ด๋„ false๊ฐ€ ๋ฐ˜ํ™˜๋˜์–ด์„œ, ์ง€๊ธˆ์€ ACCEPT_PANICKED ๋•Œ๋ฌธ์— ์ข…๋ฃŒ๋œ ๊ฒƒ์ธ์ง€ ๊ตฌ๋ถ„ํ•˜๊ธฐ ์–ด๋ ค์šธ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

None ๋Œ€์‹  Some(record)๋ฅผ ๋„˜๊ธฐ๋ฉด ๋” ํ™•์‹คํ•˜๊ฒŒ ํ™•์ธํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

@zhy2on zhy2on Oct 26, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด์ƒ ์—†์Šต๋‹ˆ๋‹ค. ๋จธ์ง€ํ•ด๋„ ๋  ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. @jhpark816

ACCEPT_PANICKED.set(true);
let mut reply: Vec<u8> = Vec::new();
let mut tokens = vec![token("vop")];
// SAFETY: `record` gets the unshared `reply` back as its cookie.
let ok = unsafe {
execute(
ptr::null(),
(&raw mut reply).cast(),
1,
tokens.as_mut_ptr(),
Some(record),
)
};
assert!(!ok);
assert!(reply.is_empty());
assert!(!ACCEPT_PANICKED.get());
}

#[test]
fn accept_clears_a_flag_left_behind() {
ACCEPT_PANICKED.set(true);
assert!(!accepts(&["get", "key"]));
assert!(!ACCEPT_PANICKED.get());
}

#[test]
fn accepts_only_vop_lines() {
assert!(accepts(&["vop", "create", "idx", "2", "0"]));
assert!(!accepts(&["get", "key"]));
assert!(!accepts(&["vopx"]));
assert!(!accepts(&[]));
}

/// # Safety
///
/// `cookie` must be an unshared `Vec<u8>`, and `dta` must hold `nbytes` bytes.
unsafe extern "C" fn record(cookie: *const c_void, nbytes: c_int, dta: *const c_char) -> bool {
// SAFETY: the caller guarantees both, per `# Safety`.
let (reply, bytes) = unsafe {
(
&mut *cookie.cast_mut().cast::<Vec<u8>>(),
std::slice::from_raw_parts(dta.cast::<u8>(), nbytes as usize),
)
};
reply.extend_from_slice(bytes);
true
}

#[test]
fn every_vop_line_is_not_supported() {
let mut reply: Vec<u8> = Vec::new();
let mut tokens = vec![token("vop"), token("create")];
// SAFETY: `record` gets the unshared `reply` back as its cookie.
let ok = unsafe {
execute(
ptr::null(),
(&raw mut reply).cast(),
2,
tokens.as_mut_ptr(),
Some(record),
)
};
assert!(ok);
assert_eq!(reply, b"NOT_SUPPORTED\r\n");
}
}
66 changes: 61 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,72 @@
//! arcus-vector: vector search as an extension to arcus-memcached.

mod ascii;
mod memcached;

pub fn add(left: u64, right: u64) -> u64 {
left + right
use std::os::raw::c_char;
use std::panic::{AssertUnwindSafe, catch_unwind};

use crate::memcached::ffi::{
EXTENSION_ERROR_CODE, EXTENSION_ERROR_CODE_EXTENSION_FATAL,
EXTENSION_ERROR_CODE_EXTENSION_SUCCESS, GET_SERVER_API,
extension_type_t_EXTENSION_ASCII_PROTOCOL,
};

pub(crate) fn guard<T>(on_panic: impl FnOnce() -> T, f: impl FnOnce() -> T) -> T {

@zhy2on zhy2on Oct 25, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rust ๊ธฐ๋ณธ ํŒจ๋‹‰ ์ฒ˜๋ฆฌ๊ธฐ๋Š” ๋ฉ”์‹œ์ง€๋ฅผ stderr๋กœ ์ถœ๋ ฅํ•˜๋Š”๋ฐ,

memcached๋ฅผ -d๋กœ ๋„์šฐ๋ฉด -v๊ฐ€ ์—†์„ ๋•Œ stderr๊ฐ€ /dev/null๋กœ ๋Œ๋ ค์ ธ์„œ ์šด์˜ ํ™˜๊ฒฝ์—์„œ๋Š” ํŒจ๋‹‰ ์œ„์น˜์™€ ๋ฉ”์‹œ์ง€๊ฐ€ ๋‚จ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์ดˆ๊ธฐํ™” ์‹œ std::panic::set_hook์œผ๋กœ ํŒจ๋‹‰ ๋ฉ”์‹œ์ง€๋ฅผ memcached logger๋กœ ๋‚จ๊ธฐ๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

์ด์Šˆ๋กœ ๋‘๊ณ  ์ถ”ํ›„์— ์ ์šฉํ•ด๋„ ์ข‹์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

@ing-eoking ing-eoking Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

์ถ”ํ›„์— logger ๋ชจ๋“ˆ์„ ์ถ”๊ฐ€ํ•˜๋Š” PR์„ ์˜ฌ๋ฆด ์˜ˆ์ •์ž…๋‹ˆ๋‹ค.
set_hook ๋ถ€๋ถ„์€ ํ•ด๋‹น PR์—์„œ ํ•จ๊ป˜ ์ฒ˜๋ฆฌ๋  ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

catch_unwind(AssertUnwindSafe(f)).unwrap_or_else(|_| on_panic())
}

#[unsafe(no_mangle)]
pub extern "C" fn memcached_extensions_initialize(
_config: *const c_char,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

memcached ์ชฝ์œผ๋กœ ํŒจ๋‹‰์ด ๋„˜์–ด๊ฐ€์ง€ ์•Š๊ฒŒ ์ตœ๋Œ€ํ•œ ๋ง‰์•„์•ผ ํ•˜๋Š” ๊ฑธ๋กœ ์•Œ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

์ง€๊ธˆ์€ ๋ฆฐํŠธ๋กœ ํŒจ๋‹‰ ์ž์ฒด๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋„๋ก ๋ง‰๊ณ  ์žˆ๊ธด ํ•œ๋ฐ, FFI ๊ฒฝ๊ณ„๋งˆ๋‹ค catch_unwind๋ฅผ ๋‘์–ด ์•ˆ์ „๋ง์„ ํ•˜๋‚˜ ๋” ๋‘๋Š” ๊ฑด ์–ด๋–จ๊นŒ์š”?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๊ฐ€๋…์„ฑ์„ ๋†’์ด๊ธฐ ์œ„ํ•ด catch_unwind๋ฅผ guard ํ•จ์ˆ˜๋กœ ๋ž˜ํ•‘ํ–ˆ์Šต๋‹ˆ๋‹ค.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

panic์ด ๋ฐœ์ƒํ•˜๊ฒŒ ๋˜๋Š” ๊ฒฝ์šฐ ์ฒ˜๋ฆฌ ํ๋ฆ„์€ ๋‹ค์Œ์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

์ง„์ž…์  ์‹คํŒจยทํŒจ๋‹‰ ์‹œ ๋ฐ˜ํ™˜ ์„œ๋ฒ„(core) ์ฒ˜๋ฆฌ
memcached_extensions_initialize EXTENSION_FATAL load_extension ์‹คํŒจ โ†’ exit(EXIT_FAILURE)๋กœ ์„œ๋ฒ„ ์‹œ์ž‘ ์ค‘๋‹จ
accept true ๋ณธ๋ฌธ์„ ์ฝ์ง€ ์•Š๊ณ  ๊ฐ™์€ ์Šค๋ ˆ๋“œ์—์„œ ๋ฐ”๋กœ execute ํ˜ธ์ถœ(์•„๋ž˜ ์ฐธ๊ณ )
execute false ํ•ด๋‹น ์—ฐ๊ฒฐ๋งŒ ๋‹ซ์Œ(conn_closing), ๋ฐ๋ชฌ๊ณผ ๋‹ค๋ฅธ ์—ฐ๊ฒฐ์€ ์œ ์ง€

accept์—์„œ ํŒจ๋‹‰์ด ๋‚ฌ์„ ๋•Œ

์ˆœ์„œ ๋™์ž‘
1 *ndata = 0์œผ๋กœ ๋˜๋Œ๋ ค ์ฝ”์–ด์—์„œ conn_nread๋กœ ๊ฐ€์ง€ ์•Š๊ฒŒ ํ•จ
2 ์Šค๋ ˆ๋“œ ๋กœ์ปฌ ํ”Œ๋ž˜๊ทธ ACCEPT_PANICKED ์„ค์ •
3 true ๋ฐ˜ํ™˜ โ†’ ์ค„์„ ์ด ํ™•์žฅ์ด ๋ฐ›์€ ๊ฒƒ์œผ๋กœ ์ฒ˜๋ฆฌ (ERROR unknown command๋ฅผ ๋ณด๋‚ด์ง€ ์•Š์Œ)
4 core๊ฐ€ ๊ฐ™์€ ์Šค๋ ˆ๋“œ์—์„œ ๊ณง๋ฐ”๋กœ execute ํ˜ธ์ถœ
5 execute๊ฐ€ ํ”Œ๋ž˜๊ทธ๋ฅผ ๋ณด๊ณ  false ๋ฐ˜ํ™˜ โ†’ ์—ฐ๊ฒฐ ์ข…๋ฃŒ, ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋ณด๋‚ธ ๋ณธ๋ฌธ์€ ์—ฐ๊ฒฐ๊ณผ ํ•จ๊ป˜ ๋ฒ„๋ ค์ง

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

accept์—์„œ ํŒจ๋‹‰ ๋ฐœ์ƒ ์‹œ์— false ๋ฆฌํ„ดํ•ด์•ผ ํ•˜์ง€ ์•Š๋‚˜์š”?
ํ˜„์žฌ extention์—์„œ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๋ช…๋ น์ธ์ง€๋ฅผ ํ™•์ธํ•˜์ง€ ๋ชปํ–ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋ฉฐ, ๋”ฐ๋ผ์„œ ๋‹ค๋ฅธ extension์—์„œ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ์ง€ ํ™•์ธํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

@zhy2on zhy2on Oct 25, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

์ฃผ์š” ํŒจ๋‹‰์€ ๋ฆฐํŠธ๋กœ ๋ง‰๊ณ  ์žˆ์–ด์„œ, ๋ฐœ์ƒํ•œ๋‹ค๋ฉด ์˜ˆ์ƒํ•˜์ง€ ๋ชปํ•œ ๋ฒ„๊ทธ์ธ ๊ฒฝ์šฐ๋ผ๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋•Œ๋Š” accept๊ฐ€ ์–ด๋””๊นŒ์ง€ ์ฒ˜๋ฆฌํ–ˆ๋Š”์ง€ ๋ฏฟ์„ ์ˆ˜ ์—†์œผ๋‹ˆ ์—ฐ๊ฒฐ์„ ๋Š๋Š” ๊ฒŒ ์•ˆ์ „ํ•ด ๋ณด์ž…๋‹ˆ๋‹ค.

๋ช…๋ น ์ด๋ฆ„ ๋น„๊ต์—์„œ๋Š” ํŒจ๋‹‰์ด ๋‚  ์—ฌ์ง€๊ฐ€ ๊ฑฐ์˜ ์—†์–ด์„œ, ํŒจ๋‹‰์ด ๋‚œ๋‹ค๋ฉด ์ด๋ฏธ vop๋กœ ํ™•์ธ๋œ ๋’ค์ผ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ๋‹ค๋ฅธ extension์ด ์ฒ˜๋ฆฌํ•˜๋„๋ก false๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒฝ์šฐ๋Š” ํฌ๊ฒŒ ๊ณ ๋ คํ•˜์ง€ ์•Š์•„๋„ ๋  ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

์˜คํžˆ๋ ค false๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉด ์ฝ”์–ด๊ฐ€ ERROR unknown command๋ฅผ ๋ณด๋‚ด๊ณ  ์—ฐ๊ฒฐ์„ ์œ ์ง€ํ•œ ์ฑ„ ๋‹ค์Œ ์ค„์„ ์ฝ์Šต๋‹ˆ๋‹ค. ์•ž์œผ๋กœ vop upsert, vop search์ฒ˜๋Ÿผ ๋ณธ๋ฌธ์ด ์žˆ๋Š” ๋ช…๋ น์ด ๋“ค์–ด์˜ค๋ฉด ๋ณธ๋ฌธ์ด ๋‹ค์Œ ๋ช…๋ น์œผ๋กœ ํ•ด์„๋˜์–ด ์‘๋‹ต์ด ์–ด๊ธ‹๋‚˜๊ฑฐ๋‚˜, ๋ณธ๋ฌธ ๋‚ด์šฉ์ด ๋‹ค๋ฅธ ๋ช…๋ น์œผ๋กœ ์‹คํ–‰๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

accept์—์„œ ์—ฐ๊ฒฐ์„ ๋Š์œผ๋ ค๋ฉด true๋กœ ๋ฐ›์€ ๋’ค execute์—์„œ false๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐฉ๋ฒ•๋ฟ์ด๋ผ, ์ง€๊ธˆ ๋ฐฉ์‹์ด ๋งž๋‹ค๊ณ  ๋ด…๋‹ˆ๋‹ค.

๋‹ค๋งŒ SASL ๊ถŒํ•œ ๊ฒ€์‚ฌ์—์„œ ๋ง‰ํžˆ๋ฉด execute๊ฐ€ ํ˜ธ์ถœ๋˜์ง€ ์•Š์•„ ACCEPT_PANICKED๊ฐ€ ๋‚จ์„ ์ˆ˜ ์žˆ์œผ๋‹ˆ, accept ์‹œ์ž‘ ์‹œ ํ”Œ๋ž˜๊ทธ๋ฅผ ์ดˆ๊ธฐํ™”ํ•ด๋‘๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

get_server_api: GET_SERVER_API,
) -> EXTENSION_ERROR_CODE {
guard(
|| EXTENSION_ERROR_CODE_EXTENSION_FATAL,
|| initialize(get_server_api),
)
}

fn initialize(get_server_api: GET_SERVER_API) -> EXTENSION_ERROR_CODE {
let Some(get_server_api) = get_server_api else {
return EXTENSION_ERROR_CODE_EXTENSION_FATAL;
};
// SAFETY: the core's own function; it takes no arguments and returns its static handle.
let server = unsafe { get_server_api() };
if server.is_null() {
return EXTENSION_ERROR_CODE_EXTENSION_FATAL;
}
// SAFETY: `server` was null-checked above and points to the core's static handle.
let extension = unsafe { (*server).extension };
if extension.is_null() {
return EXTENSION_ERROR_CODE_EXTENSION_FATAL;
}
// SAFETY: `extension` was null-checked above and points to the core's static extension table.
let Some(register) = (unsafe { (*extension).register_extension }) else {
return EXTENSION_ERROR_CODE_EXTENSION_FATAL;
};

// SAFETY: `DESCRIPTOR` is static and only the core touches it from here on (it writes `next`).
let registered = unsafe {
register(
extension_type_t_EXTENSION_ASCII_PROTOCOL,
(&raw mut ascii::DESCRIPTOR).cast(),
)
};
if registered {
EXTENSION_ERROR_CODE_EXTENSION_SUCCESS
} else {
EXTENSION_ERROR_CODE_EXTENSION_FATAL
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
fn guard_turns_a_panic_into_the_fallback() {
assert_eq!(guard(|| 0, || 1), 1);
assert_eq!(guard(|| 0, || panic!("in a callback")), 0);
}
}
Loading