Skip to content
Merged
43 changes: 26 additions & 17 deletions src/runtime/valkey_jsc/js_valkey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1586,29 +1586,38 @@ impl JSValkeyClient {
// the host-fn shim passes a bare `&self` with no ref of its own.
let _guard = self.ref_scope();

if self.client.get().status == valkey::Status::NeverConnected {
bun_core::hint::cold();

match self.connect() {
// The command is queued below as for a dial in flight; the
// deferred close then rejects it or a retry sends it, like a
// refused dial.
Err(err) => {
debug!(
"first dial failed before a socket was opened: {}",
err.name()
);
self.close_without_socket_next_tick();
}
Ok(()) => self.reset_connection_timeout(),
}
}
self.ensure_dialing();

let self_br = BackRef::new(self);
let _update = scopeguard::guard(self_br, |p| p.update_poll_ref());
self.client_mut().send(global_this, command)
}

/// Start the first dial if the client has never connected. Every command
/// entry point runs this before looking at the client's state, so a
/// command on a fresh client is queued behind a dial in flight (or
/// rejected against a dial that already failed), never against
/// `NeverConnected`.
pub(crate) fn ensure_dialing(&self) {
if self.client.get().status != valkey::Status::NeverConnected {
return;
}
bun_core::hint::cold();

match self.connect() {
// The command is queued as for a dial in flight; the deferred
// close then rejects it or a retry sends it, like a refused dial.
Err(err) => {
debug!(
"first dial failed before a socket was opened: {}",
err.name()
);
self.close_without_socket_next_tick();
}
Ok(()) => self.reset_connection_timeout(),
}
}

// Getter for memory cost - useful for diagnostics
pub(crate) fn memory_cost(&self) -> usize {
// TODO(markovejnovic): This is most-likely wrong because I didn't know better.
Expand Down
29 changes: 22 additions & 7 deletions src/runtime/valkey_jsc/js_valkey_functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1845,6 +1845,27 @@ impl JSValkeyClient {
if !handler_callback.is_callable() {
return Err(global.throw_invalid_argument_type("subscribe", "listener", "function"));
}
if !channel_or_many.is_string() && !channel_or_many.is_array() {
return Err(global.throw_invalid_argument_type(
"subscribe",
"channel",
"string or array",
));
}

// The walk below stores each listener as it goes. A client that would
// reject the SUBSCRIBE outright must not keep the listeners either: a
// listener with no subscription behind it pins the event loop and the
// client, and cannot be removed with unsubscribe(). The dial comes
// after the argument checks, as for every other command, and before
// the state check, as in `send()`, so a fresh client with the offline
// queue off is rejected the way get() is: connecting, not never
// connected.
this.ensure_dialing();
if let Some(message) = this.client.get().send_rejection() {
let error = valkey::ValkeyClient::send_rejection_error(global, message);
return Ok(JSPromise::rejected_promise(global, error).to_js());
}
Comment thread
robobun marked this conversation as resolved.

// The first argument given is the channel or may be an array of channels.
if channel_or_many.is_array() {
Expand Down Expand Up @@ -1874,20 +1895,14 @@ impl JSValkeyClient {
// handler.
this.upsert_receive_handler(global, channel_arg, handler_callback)?;
}
} else if channel_or_many.is_string() {
} else {
// It is a single string channel
let Some(channel) = from_js(global, channel_or_many)? else {
return Err(global.throw_invalid_argument_type("subscribe", "channel", "string"));
};
redis_channels.push(channel);

this.upsert_receive_handler(global, channel_or_many, handler_callback)?;
} else {
return Err(global.throw_invalid_argument_type(
"subscribe",
"channel",
"string or array",
));
}

let command = Command {
Expand Down
78 changes: 37 additions & 41 deletions src/runtime/valkey_jsc/valkey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1437,57 +1437,53 @@ impl ValkeyClient {
let mut promise = command::Promise::create(global_this, checked_command.meta);

let js_promise: *mut JSPromise = std::ptr::from_mut::<JSPromise>(promise.promise.get());
if self.flags.failed {
if let Some(message) = self.send_rejection() {
let _ = promise.reject(
global_this,
Ok(global_this
.err(
bun_jsc::ErrorCode::REDIS_CONNECTION_CLOSED,
format_args!("Connection has failed"),
)
.to_js()),
Ok(Self::send_rejection_error(global_this, message)),
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} else {
// Handle disconnected state with offline queue
match self.status {
Status::Connected => {
self.enqueue(&checked_command, promise)?;

// Schedule auto-flushing to process this command if pipelining is enabled
if self.flags.enable_auto_pipelining
&& checked_command
.meta
.contains(command::Meta::SUPPORTS_AUTO_PIPELINING)
&& self.status == Status::Connected
&& !self.queue.is_empty()
{
self.register_auto_flusher(self.vm);
}
}
Status::NeverConnected | Status::Connecting | Status::Disconnected => {
// Only queue if offline queue is enabled
if self.flags.enable_offline_queue {
self.enqueue(&checked_command, promise)?;
} else {
let _ = promise.reject(
global_this,
Ok(global_this
.err(
bun_jsc::ErrorCode::REDIS_CONNECTION_CLOSED,
format_args!(
"Connection is closed and offline queue is disabled"
),
)
.to_js()),
);
}
}
self.enqueue(&checked_command, promise)?;

// Schedule auto-flushing to process this command if pipelining is enabled
if self.flags.enable_auto_pipelining
&& checked_command
.meta
.contains(command::Meta::SUPPORTS_AUTO_PIPELINING)
&& self.status == Status::Connected
&& !self.queue.is_empty()
{
self.register_auto_flusher(self.vm);
}
}

Ok(js_promise)
}

/// Why `send()` would reject a command outright instead of sending or
/// queueing it in the current state, or `None` when it would be accepted.
pub(crate) fn send_rejection(&self) -> Option<&'static str> {
if self.flags.failed {
return Some("Connection has failed");
}
if self.status != Status::Connected && !self.flags.enable_offline_queue {
return Some("Connection is closed and offline queue is disabled");
}
None
}

pub(crate) fn send_rejection_error(
global_this: &JSGlobalObject,
message: &'static str,
) -> JSValue {
global_this
.err(
bun_jsc::ErrorCode::REDIS_CONNECTION_CLOSED,
format_args!("{message}"),
)
.to_js()
}

/// Close the Valkey connection
pub(crate) fn disconnect(&mut self) -> JsResult<()> {
self.flags.is_manually_closed = true;
Expand Down
Loading
Loading