Skip to content

Commit fa08337

Browse files
committed
Introduce Invalid pseudo-command.
This helps draw a distinction between errors caused by bugs in the server, and errors caused by bugs in the client. This latter kind, at least as `Command` parse errors are concerned, is handled now with an `Invalid` pseudo-command (akin to `Unknown`). Drawing this distinction is pedagogically useful, as it helps set up an example usage of `warn!` in `process_frame` (in contrast to other kinds of errors, which are still traced with `error!` in `Listener::run`).
1 parent fc4314d commit fa08337

File tree

9 files changed

+84
-35
lines changed

9 files changed

+84
-35
lines changed

src/cmd/get.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{Connection, Db, Frame, Parse};
1+
use crate::{Connection, Db, Frame, Parse, ParseError};
22

33
use bytes::Bytes;
44
use tracing::{debug, instrument};
@@ -48,7 +48,7 @@ impl Get {
4848
/// GET key
4949
/// ```
5050
#[instrument(level = "trace", name = "Get::parse_frames", skip(parse))]
51-
pub(crate) fn parse_frames(parse: &mut Parse) -> crate::Result<Get> {
51+
pub(crate) fn parse_frames(parse: &mut Parse) -> Result<Self, ParseError> {
5252
// The `GET` string has already been consumed. The next value is the
5353
// name of the key to get. If the next value is not a string or the
5454
// input is fully consumed, then an error is returned.

src/cmd/invalid.rs

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
use crate::{Connection, Frame, ParseError};
2+
3+
use tracing::instrument;
4+
5+
/// Represents a malformed frame. This is not a real `Redis` command.
6+
#[derive(Debug)]
7+
pub struct Invalid {
8+
error: ParseError,
9+
}
10+
11+
impl Invalid {
12+
/// Create a new `Invalid` command which responds to frames that could not
13+
/// be successfully parsed as commands.
14+
pub(crate) fn new(error: ParseError) -> Self {
15+
Self { error }
16+
}
17+
18+
/// Responds to the client, indicating the command could not be parsed.
19+
#[instrument(level = "trace", name = "ParseError::apply", skip(dst))]
20+
pub(crate) async fn apply(self, dst: &mut Connection) -> crate::Result<()> {
21+
let response = Frame::Error(self.error.to_string());
22+
dst.write_frame(&response).await?;
23+
Ok(())
24+
}
25+
}

src/cmd/mod.rs

+25-9
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ pub use ping::Ping;
1616
mod unknown;
1717
pub use unknown::Unknown;
1818

19+
mod invalid;
20+
pub use invalid::Invalid;
21+
1922
use crate::{Connection, Db, Frame, Parse, ParseError, Shutdown};
2023
use tracing::instrument;
2124

@@ -31,6 +34,7 @@ pub enum Command {
3134
Unsubscribe(Unsubscribe),
3235
Ping(Ping),
3336
Unknown(Unknown),
37+
Invalid(Invalid),
3438
}
3539

3640
impl Command {
@@ -42,8 +46,13 @@ impl Command {
4246
/// # Returns
4347
///
4448
/// On success, the command value is returned, otherwise, `Err` is returned.
45-
#[instrument(level = "trace", name = "Command::from_frame", skip(frame), err)]
46-
pub fn from_frame(frame: Frame) -> crate::Result<Command> {
49+
///
50+
/// # Traces
51+
///
52+
/// Generates a TRACE-level span named `Command::from_frame` that includes
53+
/// the `Debug`-representation of `frame` as a field.
54+
#[instrument(level = "trace", name = "Command::from_frame")]
55+
pub fn from_frame(frame: Frame) -> Result<Command, ParseError> {
4756
// The frame value is decorated with `Parse`. `Parse` provides a
4857
// "cursor" like API which makes parsing the command easier.
4958
//
@@ -87,16 +96,21 @@ impl Command {
8796
Ok(command)
8897
}
8998

99+
/// Construct an `Invalid` response command from a `ParseError`.
100+
pub(crate) fn from_error(err: ParseError) -> Command {
101+
Command::Invalid(invalid::Invalid::new(err))
102+
}
103+
90104
/// Apply the command to the specified `Db` instance.
91105
///
92106
/// The response is written to `dst`. This is called by the server in order
93107
/// to execute a received command.
94-
#[instrument(
95-
level = "trace",
96-
name = "Command::apply",
97-
skip(self, db, dst, shutdown),
98-
err
99-
)]
108+
///
109+
/// # Traces
110+
///
111+
/// Generates a `TRACE`-level span that includes the `Debug`-serializaiton
112+
/// of `self` (the `Command` being applied) as a field.
113+
#[instrument(level = "trace", name = "Command::apply", skip(db, dst, shutdown))]
100114
pub(crate) async fn apply(
101115
self,
102116
db: &Db,
@@ -112,9 +126,10 @@ impl Command {
112126
Subscribe(cmd) => cmd.apply(db, dst, shutdown).await,
113127
Ping(cmd) => cmd.apply(dst).await,
114128
Unknown(cmd) => cmd.apply(dst).await,
129+
Invalid(cmd) => cmd.apply(dst).await,
115130
// `Unsubscribe` cannot be applied. It may only be received from the
116131
// context of a `Subscribe` command.
117-
Unsubscribe(_) => Err("`Unsubscribe` is unsupported in this context".into()),
132+
Unsubscribe(_) => Result::Err("`Unsubscribe` is unsupported in this context".into()),
118133
}
119134
}
120135

@@ -128,6 +143,7 @@ impl Command {
128143
Command::Unsubscribe(_) => "unsubscribe",
129144
Command::Ping(_) => "ping",
130145
Command::Unknown(cmd) => cmd.get_name(),
146+
Command::Invalid(_) => "err",
131147
}
132148
}
133149
}

src/cmd/ping.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ impl Ping {
4040
/// PING [message]
4141
/// ```
4242
#[instrument(level = "trace", name = "Ping::parse_frames", skip(parse))]
43-
pub(crate) fn parse_frames(parse: &mut Parse) -> crate::Result<Ping> {
43+
pub(crate) fn parse_frames(parse: &mut Parse) -> Result<Self, ParseError> {
4444
match parse.next_string() {
4545
Ok(msg) => Ok(Ping::new(Some(msg))),
4646
Err(ParseError::EndOfStream) => Ok(Ping::default()),

src/cmd/publish.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{Connection, Db, Frame, Parse};
1+
use crate::{Connection, Db, Frame, Parse, ParseError};
22

33
use bytes::Bytes;
44
use tracing::instrument;
@@ -49,7 +49,7 @@ impl Publish {
4949
/// PUBLISH channel message
5050
/// ```
5151
#[instrument(level = "trace", name = "Publish::parse_frames", skip(parse))]
52-
pub(crate) fn parse_frames(parse: &mut Parse) -> crate::Result<Publish> {
52+
pub(crate) fn parse_frames(parse: &mut Parse) -> Result<Self, ParseError> {
5353
// The `PUBLISH` string has already been consumed. Extract the `channel`
5454
// and `message` values from the frame.
5555
//

src/cmd/set.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ impl Set {
7878
/// SET key value [EX seconds|PX milliseconds]
7979
/// ```
8080
#[instrument(level = "trace", name = "Set::parse_frames", skip(parse))]
81-
pub(crate) fn parse_frames(parse: &mut Parse) -> crate::Result<Set> {
81+
pub(crate) fn parse_frames(parse: &mut Parse) -> Result<Self, ParseError> {
8282
use ParseError::EndOfStream;
8383

8484
// Read the key to set. This is a required field

src/cmd/subscribe.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ impl Subscribe {
6262
/// SUBSCRIBE channel [channel ...]
6363
/// ```
6464
#[instrument(level = "trace", name = "Subscribe::parse_frames", skip(parse))]
65-
pub(crate) fn parse_frames(parse: &mut Parse) -> crate::Result<Subscribe> {
65+
pub(crate) fn parse_frames(parse: &mut Parse) -> Result<Self, ParseError> {
6666
use ParseError::EndOfStream;
6767

6868
// The `SUBSCRIBE` string has already been consumed. At this point,

src/parse.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ pub(crate) struct Parse {
2121
/// Only `EndOfStream` errors are handled at runtime. All other errors result in
2222
/// the connection being terminated.
2323
#[derive(Debug)]
24-
pub(crate) enum ParseError {
24+
pub enum ParseError {
2525
/// Attempting to extract a value failed due to the frame being fully
2626
/// consumed.
2727
EndOfStream,

src/server.rs

+26-18
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use std::sync::Arc;
1111
use tokio::net::{TcpListener, TcpStream};
1212
use tokio::sync::{broadcast, mpsc, Semaphore};
1313
use tokio::time::{self, Duration};
14-
use tracing::{debug, error, info, instrument};
14+
use tracing::{error, info, instrument, warn};
1515

1616
/// Server listener state. Created in the `run` call. It includes a `run` method
1717
/// which performs the TCP listening and initialization of per-connection state.
@@ -349,7 +349,7 @@ impl Handler {
349349
}
350350

351351
/// Process a single connection.
352-
#[instrument(level = "debug", name = "Handler::process_frame", skip(self), err)]
352+
#[instrument(level = "debug", name = "Handler::process_frame", skip(self))]
353353
async fn process_frame(&mut self) -> crate::Result<ControlFlow<(), ()>> {
354354
// While reading a request frame, also listen for the shutdown
355355
// signal.
@@ -370,23 +370,31 @@ impl Handler {
370370
None => return Ok(ControlFlow::Break(())),
371371
};
372372

373-
debug!(?frame);
374-
375373
// Convert the redis frame into a command struct. This returns an
376-
// error if the frame is not a valid redis command or it is an
377-
// unsupported command.
378-
let cmd = Command::from_frame(frame)?;
379-
380-
// Logs the `cmd` object. The syntax here is a shorthand provided by
381-
// the `tracing` crate. It can be thought of as similar to:
382-
//
383-
// ```
384-
// debug!(cmd = format!("{:?}", cmd));
385-
// ```
386-
//
387-
// `tracing` provides structured logging, so information is "logged"
388-
// as key-value pairs.
389-
debug!(?cmd);
374+
// error if the frame is not a valid redis command.
375+
let cmd = match Command::from_frame(frame) {
376+
Ok(cmd) => cmd,
377+
Err(cause) => {
378+
// The frame was malformed and could not be parsed. This is
379+
// probably indicative of an issue with the client (as opposed
380+
// to our server), so we (1) emit a warning...
381+
//
382+
// The syntax here is a shorthand provided by the `tracing`
383+
// crate. It can be thought of as similar to:
384+
// warn! {
385+
// cause = format!("{}", cause),
386+
// "failed to parse command from frame"
387+
// };
388+
// `tracing` provides structured logging, so information is
389+
// "logged" as key-value pairs.
390+
warn! {
391+
%cause,
392+
"failed to parse command from frame"
393+
};
394+
// ...and (2) respond to the client with the error:
395+
Command::from_error(cause)
396+
}
397+
};
390398

391399
// Perform the work needed to apply the command. This may mutate the
392400
// database state as a result.

0 commit comments

Comments
 (0)