diff --git a/Cargo.lock b/Cargo.lock index e910e5d..9e005dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1403,4 +1403,4 @@ checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" name = "windows_x86_64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" \ No newline at end of file diff --git a/src/cmd/get.rs b/src/cmd/get.rs index 81964a8..146d8b0 100644 --- a/src/cmd/get.rs +++ b/src/cmd/get.rs @@ -1,4 +1,4 @@ -use crate::{Connection, Db, Frame, Parse}; +use crate::{Connection, Db, Frame, Parse, ParseError}; use bytes::Bytes; use tracing::{debug, instrument}; @@ -47,7 +47,8 @@ impl Get { /// ```text /// GET key /// ``` - pub(crate) fn parse_frames(parse: &mut Parse) -> crate::Result { + #[instrument(level = "trace", name = "Get::parse_frames", skip(parse))] + pub(crate) fn parse_frames(parse: &mut Parse) -> Result { // The `GET` string has already been consumed. The next value is the // name of the key to get. If the next value is not a string or the // input is fully consumed, then an error is returned. @@ -60,7 +61,14 @@ impl Get { /// /// The response is written to `dst`. This is called by the server in order /// to execute a received command. - #[instrument(skip(self, db, dst))] + #[instrument( + level = "trace", + name = "Get::apply", + skip(self, db, dst), + fields( + key = self.key.as_str(), + ), + )] pub(crate) async fn apply(self, db: &Db, dst: &mut Connection) -> crate::Result<()> { // Get the value from the shared database state let response = if let Some(value) = db.get(&self.key) { diff --git a/src/cmd/invalid.rs b/src/cmd/invalid.rs new file mode 100644 index 0000000..8093e22 --- /dev/null +++ b/src/cmd/invalid.rs @@ -0,0 +1,25 @@ +use crate::{Connection, Frame, ParseError}; + +use tracing::instrument; + +/// Represents a malformed frame. This is not a real `Redis` command. +#[derive(Debug)] +pub struct Invalid { + error: ParseError, +} + +impl Invalid { + /// Create a new `Invalid` command which responds to frames that could not + /// be successfully parsed as commands. + pub(crate) fn new(error: ParseError) -> Self { + Self { error } + } + + /// Responds to the client, indicating the command could not be parsed. + #[instrument(level = "trace", name = "ParseError::apply", skip(dst))] + pub(crate) async fn apply(self, dst: &mut Connection) -> crate::Result<()> { + let response = Frame::Error(self.error.to_string()); + dst.write_frame(&response).await?; + Ok(()) + } +} diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 2aae08e..180c287 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -16,7 +16,11 @@ pub use ping::Ping; mod unknown; pub use unknown::Unknown; +mod invalid; +pub use invalid::Invalid; + use crate::{Connection, Db, Frame, Parse, ParseError, Shutdown}; +use tracing::instrument; /// Enumeration of supported Redis commands. /// @@ -30,6 +34,7 @@ pub enum Command { Unsubscribe(Unsubscribe), Ping(Ping), Unknown(Unknown), + Invalid(Invalid), } impl Command { @@ -41,7 +46,13 @@ impl Command { /// # Returns /// /// On success, the command value is returned, otherwise, `Err` is returned. - pub fn from_frame(frame: Frame) -> crate::Result { + /// + /// # Traces + /// + /// Generates a TRACE-level span named `Command::from_frame` that includes + /// the `Debug`-representation of `frame` as a field. + #[instrument(level = "trace", name = "Command::from_frame")] + pub fn from_frame(frame: Frame) -> Result { // The frame value is decorated with `Parse`. `Parse` provides a // "cursor" like API which makes parsing the command easier. // @@ -51,8 +62,10 @@ impl Command { // All redis commands begin with the command name as a string. The name // is read and converted to lower cases in order to do case sensitive - // matching. - let command_name = parse.next_string()?.to_lowercase(); + // matching. Doing this in-place with `str::make_ascii_lowercase` is + // substantially more performant than `str::to_lowercase`. + let mut command_name = parse.next_string()?; + command_name.make_ascii_lowercase(); // Match the command name, delegating the rest of the parsing to the // specific command. @@ -83,10 +96,21 @@ impl Command { Ok(command) } + /// Construct an `Invalid` response command from a `ParseError`. + pub(crate) fn from_error(err: ParseError) -> Command { + Command::Invalid(invalid::Invalid::new(err)) + } + /// Apply the command to the specified `Db` instance. /// /// The response is written to `dst`. This is called by the server in order /// to execute a received command. + /// + /// # Traces + /// + /// Generates a `TRACE`-level span that includes the `Debug`-serializaiton + /// of `self` (the `Command` being applied) as a field. + #[instrument(level = "trace", name = "Command::apply", skip(db, dst, shutdown))] pub(crate) async fn apply( self, db: &Db, @@ -102,9 +126,10 @@ impl Command { Subscribe(cmd) => cmd.apply(db, dst, shutdown).await, Ping(cmd) => cmd.apply(dst).await, Unknown(cmd) => cmd.apply(dst).await, + Invalid(cmd) => cmd.apply(dst).await, // `Unsubscribe` cannot be applied. It may only be received from the // context of a `Subscribe` command. - Unsubscribe(_) => Err("`Unsubscribe` is unsupported in this context".into()), + Unsubscribe(_) => Result::Err("`Unsubscribe` is unsupported in this context".into()), } } @@ -118,6 +143,7 @@ impl Command { Command::Unsubscribe(_) => "unsubscribe", Command::Ping(_) => "ping", Command::Unknown(cmd) => cmd.get_name(), + Command::Invalid(_) => "err", } } } diff --git a/src/cmd/ping.rs b/src/cmd/ping.rs index 14a467e..36b14b6 100644 --- a/src/cmd/ping.rs +++ b/src/cmd/ping.rs @@ -39,7 +39,8 @@ impl Ping { /// ```text /// PING [message] /// ``` - pub(crate) fn parse_frames(parse: &mut Parse) -> crate::Result { + #[instrument(level = "trace", name = "Ping::parse_frames", skip(parse))] + pub(crate) fn parse_frames(parse: &mut Parse) -> Result { match parse.next_string() { Ok(msg) => Ok(Ping::new(Some(msg))), Err(ParseError::EndOfStream) => Ok(Ping::default()), @@ -51,7 +52,13 @@ impl Ping { /// /// The response is written to `dst`. This is called by the server in order /// to execute a received command. - #[instrument(skip(self, dst))] + #[instrument( + name = "Ping::apply", + skip(self, dst), + fields( + msg = ?self.msg, + ), + )] pub(crate) async fn apply(self, dst: &mut Connection) -> crate::Result<()> { let response = match self.msg { None => Frame::Simple("PONG".to_string()), diff --git a/src/cmd/publish.rs b/src/cmd/publish.rs index 3c28b1c..abda68c 100644 --- a/src/cmd/publish.rs +++ b/src/cmd/publish.rs @@ -1,6 +1,7 @@ -use crate::{Connection, Db, Frame, Parse}; +use crate::{Connection, Db, Frame, Parse, ParseError}; use bytes::Bytes; +use tracing::instrument; /// Posts a message to the given channel. /// @@ -47,7 +48,8 @@ impl Publish { /// ```text /// PUBLISH channel message /// ``` - pub(crate) fn parse_frames(parse: &mut Parse) -> crate::Result { + #[instrument(level = "trace", name = "Publish::parse_frames", skip(parse))] + pub(crate) fn parse_frames(parse: &mut Parse) -> Result { // The `PUBLISH` string has already been consumed. Extract the `channel` // and `message` values from the frame. // @@ -64,6 +66,14 @@ impl Publish { /// /// The response is written to `dst`. This is called by the server in order /// to execute a received command. + #[instrument( + level = "trace", + name = "Publish::apply", + skip(self, db, dst), + fields( + channel = self.channel.as_str(), + ), + )] pub(crate) async fn apply(self, db: &Db, dst: &mut Connection) -> crate::Result<()> { // The shared state contains the `tokio::sync::broadcast::Sender` for // all active channels. Calling `db.publish` dispatches the message into diff --git a/src/cmd/set.rs b/src/cmd/set.rs index eae05d7..b5c429e 100644 --- a/src/cmd/set.rs +++ b/src/cmd/set.rs @@ -3,7 +3,7 @@ use crate::{Connection, Db, Frame}; use bytes::Bytes; use std::time::Duration; -use tracing::{debug, instrument}; +use tracing::instrument; /// Set `key` to hold the string `value`. /// @@ -77,7 +77,8 @@ impl Set { /// ```text /// SET key value [EX seconds|PX milliseconds] /// ``` - pub(crate) fn parse_frames(parse: &mut Parse) -> crate::Result { + #[instrument(level = "trace", name = "Set::parse_frames", skip(parse))] + pub(crate) fn parse_frames(parse: &mut Parse) -> Result { use ParseError::EndOfStream; // Read the key to set. This is a required field @@ -124,14 +125,21 @@ impl Set { /// /// The response is written to `dst`. This is called by the server in order /// to execute a received command. - #[instrument(skip(self, db, dst))] + #[instrument( + level = "trace", + name = "Set::apply", + skip(self, db, dst), + fields( + key = self.key.as_str(), + expire = ?self.expire.as_ref(), + ), + )] pub(crate) async fn apply(self, db: &Db, dst: &mut Connection) -> crate::Result<()> { // Set the value in the shared database state. db.set(self.key, self.value, self.expire); // Create a success response and write it to `dst`. let response = Frame::Simple("OK".to_string()); - debug!(?response); dst.write_frame(&response).await?; Ok(()) diff --git a/src/cmd/subscribe.rs b/src/cmd/subscribe.rs index 4b339c4..1603437 100644 --- a/src/cmd/subscribe.rs +++ b/src/cmd/subscribe.rs @@ -6,6 +6,7 @@ use std::pin::Pin; use tokio::select; use tokio::sync::broadcast; use tokio_stream::{Stream, StreamExt, StreamMap}; +use tracing::instrument; /// Subscribes the client to one or more channels. /// @@ -60,7 +61,8 @@ impl Subscribe { /// ```text /// SUBSCRIBE channel [channel ...] /// ``` - pub(crate) fn parse_frames(parse: &mut Parse) -> crate::Result { + #[instrument(level = "trace", name = "Subscribe::parse_frames", skip(parse))] + pub(crate) fn parse_frames(parse: &mut Parse) -> Result { use ParseError::EndOfStream; // The `SUBSCRIBE` string has already been consumed. At this point, @@ -99,6 +101,14 @@ impl Subscribe { /// are updated accordingly. /// /// [here]: https://redis.io/topics/pubsub + #[instrument( + level = "trace", + name = "Suscribe::apply", + skip(self, db, dst), + fields( + channels = "UNIMPLEMENTED", // FIXME + ), + )] pub(crate) async fn apply( mut self, db: &Db, diff --git a/src/cmd/unknown.rs b/src/cmd/unknown.rs index 25f869a..072952f 100644 --- a/src/cmd/unknown.rs +++ b/src/cmd/unknown.rs @@ -25,7 +25,14 @@ impl Unknown { /// Responds to the client, indicating the command is not recognized. /// /// This usually means the command is not yet implemented by `mini-redis`. - #[instrument(skip(self, dst))] + #[instrument( + level = "trace", + name = "Unknown::apply", + skip(self, dst), + fields( + command_name = self.command_name.as_str(), + ), + )] pub(crate) async fn apply(self, dst: &mut Connection) -> crate::Result<()> { let response = Frame::Error(format!("ERR unknown command '{}'", self.command_name)); diff --git a/src/connection.rs b/src/connection.rs index 64c11c8..82999d4 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -1,9 +1,11 @@ use crate::frame::{self, Frame}; use bytes::{Buf, BytesMut}; -use std::io::{self, Cursor}; +use std::io::{self, Cursor, ErrorKind::ConnectionReset}; +use std::net::SocketAddr; use tokio::io::{AsyncReadExt, AsyncWriteExt, BufWriter}; use tokio::net::TcpStream; +use tracing::warn; /// Send and receive `Frame` values from a remote peer. /// @@ -28,6 +30,16 @@ pub struct Connection { buffer: BytesMut, } +/// The result of [`Connection::maybe_read_bytes`]. +enum ConnectionState { + /// The connection was gracefully closed when reading was attempted. + Closed, + /// The connection was open when reading was attempted. + Open, + /// The connection was abruptly reset by the peer when reading was attempted. + Reset, +} + impl Connection { /// Create a new `Connection`, backed by `socket`. Read and write buffers /// are initialized. @@ -42,6 +54,11 @@ impl Connection { } } + /// Returns the remote address that this connection is bound to. + pub fn peer_addr(&self) -> io::Result { + self.stream.get_ref().peer_addr() + } + /// Read a single `Frame` value from the underlying stream. /// /// The function waits until it has retrieved enough data to parse a frame. @@ -63,23 +80,38 @@ impl Connection { // There is not enough buffered data to read a frame. Attempt to // read more data from the socket. - // - // On success, the number of bytes is returned. `0` indicates "end - // of stream". - if 0 == self.stream.read_buf(&mut self.buffer).await? { - // The remote closed the connection. For this to be a clean - // shutdown, there should be no data in the read buffer. If - // there is, this means that the peer closed the socket while - // sending a frame. - if self.buffer.is_empty() { + match self.maybe_read_bytes().await? { + ConnectionState::Open => continue, + ConnectionState::Closed | ConnectionState::Reset => { + if !self.buffer.is_empty() { + warn!( + incomplete = ?self.buffer, + "connection closed with incomplete frame", + ); + } return Ok(None); - } else { - return Err("connection reset by peer".into()); } } } } + /// Attempt to read bytes from the connection. + async fn maybe_read_bytes(&mut self) -> io::Result { + match self.stream.read_buf(&mut self.buffer).await { + // the connection was closed gracefully + Ok(0) => Ok(ConnectionState::Closed), + // the connection is still open + Ok(_) => Ok(ConnectionState::Open), + // the connection was closed abruptly by the peer + Err(e) if e.kind() == ConnectionReset => { + warn!("connection closed abruptly by peer"); + Ok(ConnectionState::Reset) + } + // reading failed for some other reason + Err(err) => Err(err), + } + } + /// Tries to parse a frame from the buffer. If the buffer contains enough /// data, the frame is returned and the data removed from the buffer. If not /// enough data has been buffered yet, `Ok(None)` is returned. If the diff --git a/src/db.rs b/src/db.rs index 07e33a2..af72812 100644 --- a/src/db.rs +++ b/src/db.rs @@ -4,7 +4,7 @@ use tokio::time::{self, Duration, Instant}; use bytes::Bytes; use std::collections::{BTreeMap, HashMap}; use std::sync::{Arc, Mutex}; -use tracing::debug; +use tracing::{debug, instrument}; /// A wrapper around a `Db` instance. This exists to allow orderly cleanup /// of the `Db` by signalling the background purge task to shut down when @@ -150,6 +150,7 @@ impl Db { /// Returns `None` if there is no value associated with the key. This may be /// due to never having assigned a value to the key or a previously assigned /// value expired. + #[instrument(level = "trace", name = "Db::get", skip(self))] pub(crate) fn get(&self, key: &str) -> Option { // Acquire the lock, get the entry and clone the value. // @@ -163,6 +164,7 @@ impl Db { /// Duration. /// /// If a value is already associated with the key, it is removed. + #[instrument(level = "trace", name = "Db::set", skip(self, value))] pub(crate) fn set(&self, key: String, value: Bytes, expire: Option) { let mut state = self.shared.state.lock().unwrap(); @@ -231,6 +233,7 @@ impl Db { /// /// The returned `Receiver` is used to receive values broadcast by `PUBLISH` /// commands. + #[instrument(level = "trace", name = "Db::subscribe", skip(self, key))] pub(crate) fn subscribe(&self, key: String) -> broadcast::Receiver { use std::collections::hash_map::Entry; @@ -262,6 +265,7 @@ impl Db { /// Publish a message to the channel. Returns the number of subscribers /// listening on the channel. + #[instrument(level = "trace", name = "Db::publish", skip(self, value))] pub(crate) fn publish(&self, key: &str, value: Bytes) -> usize { let state = self.shared.state.lock().unwrap(); @@ -279,6 +283,7 @@ impl Db { /// Signals the purge background task to shut down. This is called by the /// `DbShutdown`s `Drop` implementation. + #[instrument(name = "Db::shutdown_purge_task", skip(self))] fn shutdown_purge_task(&self) { // The background task must be signaled to shut down. This is done by // setting `State::shutdown` to `true` and signalling the task. @@ -296,6 +301,7 @@ impl Db { impl Shared { /// Purge all expired keys and return the `Instant` at which the **next** /// key will expire. The background task will sleep until this instant. + #[instrument(name = "Shared::purge_expired_keys", skip(self))] fn purge_expired_keys(&self) -> Option { let mut state = self.state.lock().unwrap(); @@ -323,6 +329,7 @@ impl Shared { } // The key expired, remove it + debug!(key = &key.as_str(), "purged_expired_key"); state.entries.remove(key); state.expirations.remove(&(when, id)); } @@ -352,6 +359,7 @@ impl State { /// /// Wait to be notified. On notification, purge any expired keys from the shared /// state handle. If `shutdown` is set, terminate the task. +#[instrument(skip(shared))] async fn purge_expired_tasks(shared: Arc) { // If the shutdown flag is set, then the task should exit. while !shared.is_shutdown() { diff --git a/src/parse.rs b/src/parse.rs index f2a73b5..a4f0367 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -2,6 +2,7 @@ use crate::Frame; use bytes::Bytes; use std::{fmt, str, vec}; +use tracing::instrument; /// Utility for parsing a command /// @@ -20,7 +21,7 @@ pub(crate) struct Parse { /// Only `EndOfStream` errors are handled at runtime. All other errors result in /// the connection being terminated. #[derive(Debug)] -pub(crate) enum ParseError { +pub enum ParseError { /// Attempting to extract a value failed due to the frame being fully /// consumed. EndOfStream, @@ -33,6 +34,7 @@ impl Parse { /// Create a new `Parse` to parse the contents of `frame`. /// /// Returns `Err` if `frame` is not an array frame. + #[instrument(level = "trace", name = "Parse::new")] pub(crate) fn new(frame: Frame) -> Result { let array = match frame { Frame::Array(array) => array, @@ -46,6 +48,7 @@ impl Parse { /// Return the next entry. Array frames are arrays of frames, so the next /// entry is a frame. + #[instrument(level = "trace", name = "Parse::next", skip(self))] fn next(&mut self) -> Result { self.parts.next().ok_or(ParseError::EndOfStream) } @@ -53,6 +56,7 @@ impl Parse { /// Return the next entry as a string. /// /// If the next entry cannot be represented as a String, then an error is returned. + #[instrument(level = "trace", name = "Parse::next_string", skip(self))] pub(crate) fn next_string(&mut self) -> Result { match self.next()? { // Both `Simple` and `Bulk` representation may be strings. Strings @@ -76,6 +80,7 @@ impl Parse { /// /// If the next entry cannot be represented as raw bytes, an error is /// returned. + #[instrument(level = "trace", name = "Parse::next_bytes", skip(self))] pub(crate) fn next_bytes(&mut self) -> Result { match self.next()? { // Both `Simple` and `Bulk` representation may be raw bytes. @@ -99,6 +104,7 @@ impl Parse { /// /// If the next entry cannot be represented as an integer, then an error is /// returned. + #[instrument(level = "trace", name = "Parse::next_int", skip(self))] pub(crate) fn next_int(&mut self) -> Result { use atoi::atoi; @@ -116,6 +122,7 @@ impl Parse { } /// Ensure there are no more entries in the array + #[instrument(level = "trace", name = "Parse::finish", skip(self))] pub(crate) fn finish(&mut self) -> Result<(), ParseError> { if self.parts.next().is_none() { Ok(()) diff --git a/src/server.rs b/src/server.rs index 2868b26..7a8dcc1 100644 --- a/src/server.rs +++ b/src/server.rs @@ -6,11 +6,12 @@ use crate::{Command, Connection, Db, DbDropGuard, Shutdown}; use std::future::Future; +use std::ops::ControlFlow; use std::sync::Arc; use tokio::net::{TcpListener, TcpStream}; use tokio::sync::{broadcast, mpsc, Semaphore}; use tokio::time::{self, Duration}; -use tracing::{debug, error, info, instrument}; +use tracing::{error, info, instrument, warn}; /// Server listener state. Created in the `run` call. It includes a `run` method /// which performs the TCP listening and initialization of per-connection state. @@ -317,57 +318,82 @@ impl Handler { /// /// When the shutdown signal is received, the connection is processed until /// it reaches a safe state, at which point it is terminated. - #[instrument(skip(self))] + #[instrument( + level = "info", + name = "Handler::run", + skip(self), + fields( + peer_addr = %self.connection.peer_addr().unwrap(), + ), + )] async fn run(&mut self) -> crate::Result<()> { // As long as the shutdown signal has not been received, try to read a // new request frame. while !self.shutdown.is_shutdown() { - // While reading a request frame, also listen for the shutdown - // signal. - let maybe_frame = tokio::select! { - res = self.connection.read_frame() => res?, - _ = self.shutdown.recv() => { - // If a shutdown signal is received, return from `run`. - // This will result in the task terminating. - return Ok(()); - } - }; - - // If `None` is returned from `read_frame()` then the peer closed - // the socket. There is no further work to do and the task can be - // terminated. - let frame = match maybe_frame { - Some(frame) => frame, - None => return Ok(()), - }; - - // Convert the redis frame into a command struct. This returns an - // error if the frame is not a valid redis command or it is an - // unsupported command. - let cmd = Command::from_frame(frame)?; - - // Logs the `cmd` object. The syntax here is a shorthand provided by - // the `tracing` crate. It can be thought of as similar to: - // - // ``` - // debug!(cmd = format!("{:?}", cmd)); - // ``` - // - // `tracing` provides structured logging, so information is "logged" - // as key-value pairs. - debug!(?cmd); - - // Perform the work needed to apply the command. This may mutate the - // database state as a result. - // - // The connection is passed into the apply function which allows the - // command to write response frames directly to the connection. In - // the case of pub/sub, multiple frames may be send back to the - // peer. - cmd.apply(&self.db, &mut self.connection, &mut self.shutdown) - .await?; + match self.process_frame().await? { + ControlFlow::Continue(..) => continue, + ControlFlow::Break(..) => return Ok(()), + } } Ok(()) } + + /// Process a single connection. + #[instrument(level = "debug", name = "Handler::process_frame", skip(self))] + async fn process_frame(&mut self) -> crate::Result> { + // While reading a request frame, also listen for the shutdown + // signal. + let maybe_frame = tokio::select! { + res = self.connection.read_frame() => res?, + _ = self.shutdown.recv() => { + // If a shutdown signal is received, return from `run`. + // This will result in the task terminating. + return Ok(ControlFlow::Break(())); + } + }; + + // If `None` is returned from `read_frame()` then the peer closed + // the socket. There is no further work to do and the task can be + // terminated. + let frame = match maybe_frame { + Some(frame) => frame, + None => return Ok(ControlFlow::Break(())), + }; + + // Convert the redis frame into a command struct. This returns an + // error if the frame is not a valid redis command. + let cmd = match Command::from_frame(frame) { + Ok(cmd) => cmd, + Err(cause) => { + // The frame was malformed and could not be parsed. This is + // probably indicative of an issue with the client (as opposed + // to our server), so we (1) emit a warning... + // + // The syntax here is a shorthand provided by the `tracing` + // crate. It can be thought of as similar to: + // warn! { + // cause = format!("{}", cause), + // "failed to parse command from frame" + // }; + // `tracing` provides structured logging, so information is + // "logged" as key-value pairs. + warn!(%cause, "failed to parse command from frame"); + // ...and (2) respond to the client with the error: + Command::from_error(cause) + } + }; + + // Perform the work needed to apply the command. This may mutate the + // database state as a result. + // + // The connection is passed into the apply function which allows the + // command to write response frames directly to the connection. In + // the case of pub/sub, multiple frames may be send back to the + // peer. + cmd.apply(&self.db, &mut self.connection, &mut self.shutdown) + .await?; + + Ok(ControlFlow::Continue(())) + } }