1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338
/// Implementors of this trait handle [`LobstersRequest`] that correspond to "real" lobste.rs /// website requests queued up by the workload generator. /// /// Note that the workload generator does not check that the implementor correctly perform the /// queries corresponding to each request; this must be verified with manual inspection of the /// Rails application or its query logs. pub trait LobstersClient { /// Errors produced by the client. /// /// If any errors are produced, they are generally printed rather than returned. type Error: std::fmt::Debug + Send + 'static; /// A future that will resolve once setup has finished. // NOTE: these should be IntoFuture, but then we can't give the Send bound type SetupFuture: futures::Future<Item = (), Error = Self::Error> + Send + 'static; /// A future that will resolve once a request has finished processing. type RequestFuture: futures::Future<Item = (), Error = Self::Error> + Send + 'static; /// Set up a fresh instance of the backend before priming. /// /// Implementing this allows benchmarking a backend without ever running the lobste.rs /// application. Normally, a backend would need to run the lobsters setup routine: /// /// ```console /// $ rails db:drop /// $ rails db:create /// $ rails db:schema:load /// $ rails db:seed /// ``` /// /// The default implementation of this method just prints an informational message saying that /// the backend was not re-created. fn setup(&mut self) -> Self::SetupFuture; /// Handle the given lobste.rs request, made on behalf of the given user, /// returning a future that resolves when the request has been satisfied. fn handle(&mut self, user: Option<UserId>, request: LobstersRequest) -> Self::RequestFuture; } /// A unique lobste.rs six-character story id. pub type StoryId = [u8; 6]; /// A unique lobste.rs six-character comment id. pub type CommentId = [u8; 6]; /// A unique lobste.rs user id. /// /// Implementors should have a reliable mapping betwen user id and username in both directions. /// This type is used both in the context of a username (e.g., /u/<user>) and in the context of who /// performed an action (e.g., POST /s/ as <user>). In the former case, <user> is a username, and /// the database will have to do a lookup based on username. In the latter, the user id is /// associated with some session, and the backend does not need to do a lookup. /// /// In the future, it is likely that this type will be split into two types: one for "session key" and /// one for "username", both of which will require lookups, but on different keys. pub type UserId = u32; /// An up or down vote. #[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum Vote { /// Upvote Up, /// Downvote Down, } /// A single lobste.rs client request. /// /// Note that one request may end up issuing multiple backend queries. To see which queries are /// executed by the real lobste.rs, see the [lobste.rs source /// code](https://github.com/lobsters/lobsters). /// /// Any request type that mentions an "acting" user is guaranteed to have the `user` argument to /// `handle` be `Some`. #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum LobstersRequest { /// Render [the frontpage](https://lobste.rs/). Frontpage, /// Render [recently submitted stories](https://lobste.rs/recent). Recent, /// Render [recently submitted comments](https://lobste.rs/comments). Comments, /// Render a [user's profile](https://lobste.rs/u/jonhoo). /// /// Note that the id here should be treated as a username. User(UserId), /// Render a [particular story](https://lobste.rs/s/cqnzl5/). Story(StoryId), /// Log in the acting user. /// /// Note that a user need not be logged in by a `LobstersRequest::Login` in order for a /// user-action (like `LobstersRequest::Submit`) to be issued for that user. The id here should /// be considered *both* a username *and* an id. The user with the username derived from this /// id should have the given id. Login, /// Log out the acting user. Logout, /// Have the acting user issue an up or down vote for the given story. /// /// Note that the load generator does not guarantee that a given user will only issue a single /// vote for a given story, nor that they will issue an equivalent number of upvotes and /// downvotes for a given story. StoryVote(StoryId, Vote), /// Have the acting user issue an up or down vote for the given comment. /// /// Note that the load generator does not guarantee that a given user will only issue a single /// vote for a given comment, nor that they will issue an equivalent number of upvotes and /// downvotes for a given comment. CommentVote(CommentId, Vote), /// Have the acting user submit a new story to the site. /// /// Note that the *generator* dictates the ids of new stories so that it can more easily keep /// track of which stories exist, and thus which stories can be voted for or commented on. Submit { /// The new story's id. id: StoryId, /// The story's title. title: String, }, /// Have the acting user submit a new comment to the given story. /// /// Note that the *generator* dictates the ids of new comments so that it can more easily keep /// track of which comments exist for the purposes of generating comment votes and deeper /// threads. Comment { /// The new comment's id. id: CommentId, /// The story the comment is for. story: StoryId, /// The id of the comment's parent comment, if any. parent: Option<CommentId>, }, } use std::mem; use std::vec; impl LobstersRequest { /// Enumerate all possible request types in a deterministic order. pub fn all() -> vec::IntoIter<mem::Discriminant<Self>> { vec![ mem::discriminant(&LobstersRequest::Story([0; 6])), mem::discriminant(&LobstersRequest::Frontpage), mem::discriminant(&LobstersRequest::User(0)), mem::discriminant(&LobstersRequest::Comments), mem::discriminant(&LobstersRequest::Recent), mem::discriminant(&LobstersRequest::CommentVote([0; 6], Vote::Up)), mem::discriminant(&LobstersRequest::StoryVote([0; 6], Vote::Up)), mem::discriminant(&LobstersRequest::Comment { id: [0; 6], story: [0; 6], parent: None, }), mem::discriminant(&LobstersRequest::Login), mem::discriminant(&LobstersRequest::Submit { id: [0; 6], title: String::new(), }), mem::discriminant(&LobstersRequest::Logout), ] .into_iter() } /// Give a textual representation of the given `LobstersRequest` discriminant. /// /// Useful for printing the keys of the maps of histograms returned by `run`. pub fn variant_name(v: &mem::Discriminant<Self>) -> &'static str { match *v { d if d == mem::discriminant(&LobstersRequest::Frontpage) => "Frontpage", d if d == mem::discriminant(&LobstersRequest::Recent) => "Recent", d if d == mem::discriminant(&LobstersRequest::Comments) => "Comments", d if d == mem::discriminant(&LobstersRequest::User(0)) => "User", d if d == mem::discriminant(&LobstersRequest::Story([0; 6])) => "Story", d if d == mem::discriminant(&LobstersRequest::Login) => "Login", d if d == mem::discriminant(&LobstersRequest::Logout) => "Logout", d if d == mem::discriminant(&LobstersRequest::StoryVote([0; 6], Vote::Up)) => { "StoryVote" } d if d == mem::discriminant(&LobstersRequest::CommentVote([0; 6], Vote::Up)) => { "CommentVote" } d if d == mem::discriminant(&LobstersRequest::Submit { id: [0; 6], title: String::new(), }) => { "Submit" } d if d == mem::discriminant(&LobstersRequest::Comment { id: [0; 6], story: [0; 6], parent: None, }) => { "Comment" } _ => unreachable!(), } } /// Produce a textual representation of this request. /// /// These are on the form: /// /// ```text /// METHOD /path [params] <user> /// ``` /// /// Where: /// /// - `METHOD` is `GET` or `POST`. /// - `/path` is the approximate lobste.rs URL endpoint for the request. /// - `[params]` are any additional params to the request such as id to assign or associate a /// new resource with with. pub fn describe(&self) -> String { match *self { LobstersRequest::Frontpage => String::from("GET /"), LobstersRequest::Recent => String::from("GET /recent"), LobstersRequest::Comments => String::from("GET /comments"), LobstersRequest::User(uid) => format!("GET /u/#{}", uid), LobstersRequest::Story(ref slug) => { format!("GET /s/{}", ::std::str::from_utf8(&slug[..]).unwrap()) } LobstersRequest::Login => String::from("POST /login"), LobstersRequest::Logout => String::from("POST /logout"), LobstersRequest::StoryVote(ref story, v) => format!( "POST /stories/{}/{}", ::std::str::from_utf8(&story[..]).unwrap(), match v { Vote::Up => "upvote", Vote::Down => "downvote", }, ), LobstersRequest::CommentVote(ref comment, v) => format!( "POST /comments/{}/{}", ::std::str::from_utf8(&comment[..]).unwrap(), match v { Vote::Up => "upvote", Vote::Down => "downvote", }, ), LobstersRequest::Submit { ref id, .. } => format!( "POST /stories [{}]", ::std::str::from_utf8(&id[..]).unwrap(), ), LobstersRequest::Comment { ref id, ref story, ref parent, } => match *parent { Some(ref parent) => format!( "POST /comments/{} [{}; {}]", ::std::str::from_utf8(&parent[..]).unwrap(), ::std::str::from_utf8(&id[..]).unwrap(), ::std::str::from_utf8(&story[..]).unwrap(), ), None => format!( "POST /comments [{}; {}]", ::std::str::from_utf8(&id[..]).unwrap(), ::std::str::from_utf8(&story[..]).unwrap(), ), }, } } } #[cfg(test)] mod tests { use super::*; #[test] fn textual_requests() { assert_eq!(LobstersRequest::Frontpage.describe(), "GET /"); assert_eq!(LobstersRequest::Recent.describe(), "GET /recent"); assert_eq!(LobstersRequest::Comments.describe(), "GET /comments"); assert_eq!(LobstersRequest::User(3).describe(), "GET /u/#3"); assert_eq!( LobstersRequest::Story([48, 48, 48, 48, 57, 97]).describe(), "GET /s/00009a" ); assert_eq!(LobstersRequest::Login.describe(), "POST /login"); assert_eq!(LobstersRequest::Logout.describe(), "POST /logout"); assert_eq!( LobstersRequest::StoryVote([48, 48, 48, 98, 57, 97], Vote::Up).describe(), "POST /stories/000b9a/upvote" ); assert_eq!( LobstersRequest::StoryVote([48, 48, 48, 98, 57, 97], Vote::Down).describe(), "POST /stories/000b9a/downvote" ); assert_eq!( LobstersRequest::CommentVote([48, 48, 48, 98, 57, 97], Vote::Up).describe(), "POST /comments/000b9a/upvote" ); assert_eq!( LobstersRequest::CommentVote([48, 48, 48, 98, 57, 97], Vote::Down).describe(), "POST /comments/000b9a/downvote" ); assert_eq!( LobstersRequest::Submit { id: [48, 48, 48, 48, 57, 97], title: String::from("foo"), } .describe(), "POST /stories [00009a]" ); assert_eq!( LobstersRequest::Comment { id: [48, 48, 48, 48, 57, 97], story: [48, 48, 48, 48, 57, 98], parent: Some([48, 48, 48, 48, 57, 99]), } .describe(), "POST /comments/00009c [00009a; 00009b]" ); assert_eq!( LobstersRequest::Comment { id: [48, 48, 48, 48, 57, 97], story: [48, 48, 48, 48, 57, 98], parent: None, } .describe(), "POST /comments [00009a; 00009b]" ); } }