{"action":"created","comment":{"url":"https://api.github.com/repos/portier/portier-broker/pulls/comments/89063056","pull_request_review_id":9613408,"id":89063056,"diff_hunk":"@@ -0,0 +1,118 @@\n+use std::error::Error;\n+use std::str::FromStr;\n+use std::time::{SystemTime, UNIX_EPOCH};\n+use super::redis::{self, PipelineCommands};\n+use super::error::BrokerResult;\n+use super::store::Store;\n+use super::serde::de::{Deserialize, Deserializer, Error as SerdeError};\n+\n+\n+/// Represents a limit configuration.\n+///\n+/// Typically provided as a string `<name>:<max_count>:<timespan>:<granularity>`.\n+#[derive(Clone,Debug)]\n+pub struct LimitConfig {\n+    /// A name for this config, appended to the Redis key.\n+    pub name: String,\n+    /// Maximum request count within the window before we refuse.\n+    pub max_count: u64,\n+    /// Timespan of the entire window, in seconds.\n+    pub timespan: u64,\n+    /// Timespan of a single bucket in the window, which defines the\n+    /// time granularity with which we store request counts.\n+    pub granularity: u64,\n+}\n+\n+impl FromStr for LimitConfig {\n+    type Err = String;\n+    fn from_str(s: &str) -> Result<LimitConfig, String> {\n+        let parts: Vec<&str> = s.split(\":\").collect();\n+        if parts.len() != 4 {\n+            return Err(\"Limit config must be 4 colon-separated parts\".to_string());\n+        }\n+\n+        let max_count = parts[1].parse::<u64>().map_err(|e| e.description().to_string())?;\n+        let timespan = parts[2].parse::<u64>().map_err(|e| e.description().to_string())?;\n+        let granularity = parts[3].parse::<u64>().map_err(|e| e.description().to_string())?;\n+        if timespan % granularity != 0 {\n+            return Err(\"Limit timespan must be a multiple of granularity\".to_string());\n+        }\n+\n+        Ok(LimitConfig {\n+            name: parts[0].to_string(),\n+            max_count: max_count,\n+            timespan: timespan,\n+            granularity: granularity,\n+        })\n+    }\n+}\n+\n+impl Deserialize for LimitConfig {\n+    fn deserialize<D: Deserializer>(deserializer: &mut D) -> Result<LimitConfig, D::Error> {\n+        let s = String::deserialize(deserializer)?;\n+        LimitConfig::from_str(&s).map_err(|msg| D::Error::custom(msg))\n+    }\n+}\n+\n+\n+/// Represents a Redis key.\n+pub enum LimitKey<'a> {\n+    Auth { email: &'a str },\n+    AuthEmail { email: &'a str },\n+}\n+\n+impl<'a> LimitKey<'a> {\n+    fn to_string(&self, config: &LimitConfig) -> String {\n+        match *self {\n+            LimitKey::Auth { email } => {\n+                format!(\"limit:auth:{}:{}\", email, &config.name)\n+            },\n+            LimitKey::AuthEmail { email } => {\n+                format!(\"limit:auth-email:{}:{}\", email, &config.name)\n+            },\n+        }\n+    }\n+}\n+\n+\n+/// Increment the given key, and test if the counter is within limits.\n+///\n+/// The Redis hash key represents a ring of counters. Each field in the hash counts requests\n+/// for a small amount of time specified by the granularity. The sum of all hash fields then\n+/// represents the count for the full configured timespan.\n+///\n+/// The hash has a TTL equal to the timespan, which is updated on every request, but individual\n+/// hash fields must also expire. To accomplish this, we double the size of the hash, and clear\n+/// the 'upcoming' half as we go. Thus, we'll never circle around to old counters before the\n+/// key TTL expires.\n+pub fn incr_and_test_limits(store: &Store, key: LimitKey, configs: &[LimitConfig]) -> BrokerResult<bool> {\n+\n+    let unixtime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();\n+    let mut result = true;\n+\n+    for ref config in configs {\n+        let key_str = key.to_string(config);\n+\n+        let num_buckets = config.timespan / config.granularity * 2;\n+        let bucket = (unixtime / config.granularity) % num_buckets;\n+\n+        // Hash fields to delete, for the upcoming half of the counter ring.\n+        let hdel_fields: Vec<u64> = (0..num_buckets / 2)\n+            .map(|i| (bucket + 1 + i) % num_buckets)\n+            .collect();\n+\n+        let (counts,): (Vec<u64>,) = redis::pipe()\n+            .atomic()\n+            .hincr(&key_str, bucket, 1).ignore()\n+            .hdel(&key_str, hdel_fields).ignore()\n+            .expire_at(&key_str, (unixtime + config.timespan) as usize).ignore()\n+            .hvals(&key_str)\n+            .query(&store.client)?;","path":"src/store_limits.rs","position":110,"original_position":110,"commit_id":"ae1e0abca599ee94bfc69f7227ecfe1bbc93f8a6","original_commit_id":"ae1e0abca599ee94bfc69f7227ecfe1bbc93f8a6","user":{"login":"stephank","id":89950,"avatar_url":"https://avatars.githubusercontent.com/u/89950?v=3","gravatar_id":"","url":"https://api.github.com/users/stephank","html_url":"https://github.com/stephank","followers_url":"https://api.github.com/users/stephank/followers","following_url":"https://api.github.com/users/stephank/following{/other_user}","gists_url":"https://api.github.com/users/stephank/gists{/gist_id}","starred_url":"https://api.github.com/users/stephank/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/stephank/subscriptions","organizations_url":"https://api.github.com/users/stephank/orgs","repos_url":"https://api.github.com/users/stephank/repos","events_url":"https://api.github.com/users/stephank/events{/privacy}","received_events_url":"https://api.github.com/users/stephank/received_events","type":"User","site_admin":false},"body":"Four times, as in, four limit configs? The overhead of the individual queries in a single pipelined transaction is probably not bad. Not sure what the right tradeoff is otherwise.","created_at":"2016-11-22T08:30:08Z","updated_at":"2016-11-22T08:30:08Z","html_url":"https://github.com/portier/portier-broker/pull/109#discussion_r89063056","pull_request_url":"https://api.github.com/repos/portier/portier-broker/pulls/109","_links":{"self":{"href":"https://api.github.com/repos/portier/portier-broker/pulls/comments/89063056"},"html":{"href":"https://github.com/portier/portier-broker/pull/109#discussion_r89063056"},"pull_request":{"href":"https://api.github.com/repos/portier/portier-broker/pulls/109"}}},"pull_request":{"url":"https://api.github.com/repos/portier/portier-broker/pulls/109","id":94385904,"html_url":"https://github.com/portier/portier-broker/pull/109","diff_url":"https://github.com/portier/portier-broker/pull/109.diff","patch_url":"https://github.com/portier/portier-broker/pull/109.patch","issue_url":"https://api.github.com/repos/portier/portier-broker/issues/109","number":109,"state":"open","locked":false,"title":"Add auth per email rate limit.","user":{"login":"stephank","id":89950,"avatar_url":"https://avatars.githubusercontent.com/u/89950?v=3","gravatar_id":"","url":"https://api.github.com/users/stephank","html_url":"https://github.com/stephank","followers_url":"https://api.github.com/users/stephank/followers","following_url":"https://api.github.com/users/stephank/following{/other_user}","gists_url":"https://api.github.com/users/stephank/gists{/gist_id}","starred_url":"https://api.github.com/users/stephank/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/stephank/subscriptions","organizations_url":"https://api.github.com/users/stephank/orgs","repos_url":"https://api.github.com/users/stephank/repos","events_url":"https://api.github.com/users/stephank/events{/privacy}","received_events_url":"https://api.github.com/users/stephank/received_events","type":"User","site_admin":false},"body":"First stab at an implementation of #70.\r\n\r\nThis is the third implementation option mentioned there, with I believe some small improvement. The details are explained here: https://github.com/portier/portier-broker/blob/d2374ae9ac2e0e3e306ce60fb5f666d8975ab406/src/store_limits.rs#L77-L86","created_at":"2016-11-18T15:52:23Z","updated_at":"2016-11-22T08:30:08Z","closed_at":null,"merged_at":null,"merge_commit_sha":"b81df2dd079229bfc8eafd17339aa7f1ffb8648c","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/portier/portier-broker/pulls/109/commits","review_comments_url":"https://api.github.com/repos/portier/portier-broker/pulls/109/comments","review_comment_url":"https://api.github.com/repos/portier/portier-broker/pulls/comments{/number}","comments_url":"https://api.github.com/repos/portier/portier-broker/issues/109/comments","statuses_url":"https://api.github.com/repos/portier/portier-broker/statuses/ae1e0abca599ee94bfc69f7227ecfe1bbc93f8a6","head":{"label":"portier:featureRateLimit","ref":"featureRateLimit","sha":"ae1e0abca599ee94bfc69f7227ecfe1bbc93f8a6","user":{"login":"portier","id":16453528,"avatar_url":"https://avatars.githubusercontent.com/u/16453528?v=3","gravatar_id":"","url":"https://api.github.com/users/portier","html_url":"https://github.com/portier","followers_url":"https://api.github.com/users/portier/followers","following_url":"https://api.github.com/users/portier/following{/other_user}","gists_url":"https://api.github.com/users/portier/gists{/gist_id}","starred_url":"https://api.github.com/users/portier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/portier/subscriptions","organizations_url":"https://api.github.com/users/portier/orgs","repos_url":"https://api.github.com/users/portier/repos","events_url":"https://api.github.com/users/portier/events{/privacy}","received_events_url":"https://api.github.com/users/portier/received_events","type":"Organization","site_admin":false},"repo":{"id":59147712,"name":"portier-broker","full_name":"portier/portier-broker","owner":{"login":"portier","id":16453528,"avatar_url":"https://avatars.githubusercontent.com/u/16453528?v=3","gravatar_id":"","url":"https://api.github.com/users/portier","html_url":"https://github.com/portier","followers_url":"https://api.github.com/users/portier/followers","following_url":"https://api.github.com/users/portier/following{/other_user}","gists_url":"https://api.github.com/users/portier/gists{/gist_id}","starred_url":"https://api.github.com/users/portier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/portier/subscriptions","organizations_url":"https://api.github.com/users/portier/orgs","repos_url":"https://api.github.com/users/portier/repos","events_url":"https://api.github.com/users/portier/events{/privacy}","received_events_url":"https://api.github.com/users/portier/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/portier/portier-broker","description":"Portier Broker reference implementation, written in Rust","fork":false,"url":"https://api.github.com/repos/portier/portier-broker","forks_url":"https://api.github.com/repos/portier/portier-broker/forks","keys_url":"https://api.github.com/repos/portier/portier-broker/keys{/key_id}","collaborators_url":"https://api.github.com/repos/portier/portier-broker/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/portier/portier-broker/teams","hooks_url":"https://api.github.com/repos/portier/portier-broker/hooks","issue_events_url":"https://api.github.com/repos/portier/portier-broker/issues/events{/number}","events_url":"https://api.github.com/repos/portier/portier-broker/events","assignees_url":"https://api.github.com/repos/portier/portier-broker/assignees{/user}","branches_url":"https://api.github.com/repos/portier/portier-broker/branches{/branch}","tags_url":"https://api.github.com/repos/portier/portier-broker/tags","blobs_url":"https://api.github.com/repos/portier/portier-broker/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/portier/portier-broker/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/portier/portier-broker/git/refs{/sha}","trees_url":"https://api.github.com/repos/portier/portier-broker/git/trees{/sha}","statuses_url":"https://api.github.com/repos/portier/portier-broker/statuses/{sha}","languages_url":"https://api.github.com/repos/portier/portier-broker/languages","stargazers_url":"https://api.github.com/repos/portier/portier-broker/stargazers","contributors_url":"https://api.github.com/repos/portier/portier-broker/contributors","subscribers_url":"https://api.github.com/repos/portier/portier-broker/subscribers","subscription_url":"https://api.github.com/repos/portier/portier-broker/subscription","commits_url":"https://api.github.com/repos/portier/portier-broker/commits{/sha}","git_commits_url":"https://api.github.com/repos/portier/portier-broker/git/commits{/sha}","comments_url":"https://api.github.com/repos/portier/portier-broker/comments{/number}","issue_comment_url":"https://api.github.com/repos/portier/portier-broker/issues/comments{/number}","contents_url":"https://api.github.com/repos/portier/portier-broker/contents/{+path}","compare_url":"https://api.github.com/repos/portier/portier-broker/compare/{base}...{head}","merges_url":"https://api.github.com/repos/portier/portier-broker/merges","archive_url":"https://api.github.com/repos/portier/portier-broker/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/portier/portier-broker/downloads","issues_url":"https://api.github.com/repos/portier/portier-broker/issues{/number}","pulls_url":"https://api.github.com/repos/portier/portier-broker/pulls{/number}","milestones_url":"https://api.github.com/repos/portier/portier-broker/milestones{/number}","notifications_url":"https://api.github.com/repos/portier/portier-broker/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/portier/portier-broker/labels{/name}","releases_url":"https://api.github.com/repos/portier/portier-broker/releases{/id}","deployments_url":"https://api.github.com/repos/portier/portier-broker/deployments","created_at":"2016-05-18T20:10:35Z","updated_at":"2016-11-21T08:41:41Z","pushed_at":"2016-11-22T07:58:04Z","git_url":"git://github.com/portier/portier-broker.git","ssh_url":"git@github.com:portier/portier-broker.git","clone_url":"https://github.com/portier/portier-broker.git","svn_url":"https://github.com/portier/portier-broker","homepage":"http://portier.github.io/","size":401,"stargazers_count":224,"watchers_count":224,"language":"Rust","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":3,"mirror_url":null,"open_issues_count":24,"forks":3,"open_issues":24,"watchers":224,"default_branch":"master"}},"base":{"label":"portier:master","ref":"master","sha":"4d9454941011098a71064e8cbb6ee29806103301","user":{"login":"portier","id":16453528,"avatar_url":"https://avatars.githubusercontent.com/u/16453528?v=3","gravatar_id":"","url":"https://api.github.com/users/portier","html_url":"https://github.com/portier","followers_url":"https://api.github.com/users/portier/followers","following_url":"https://api.github.com/users/portier/following{/other_user}","gists_url":"https://api.github.com/users/portier/gists{/gist_id}","starred_url":"https://api.github.com/users/portier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/portier/subscriptions","organizations_url":"https://api.github.com/users/portier/orgs","repos_url":"https://api.github.com/users/portier/repos","events_url":"https://api.github.com/users/portier/events{/privacy}","received_events_url":"https://api.github.com/users/portier/received_events","type":"Organization","site_admin":false},"repo":{"id":59147712,"name":"portier-broker","full_name":"portier/portier-broker","owner":{"login":"portier","id":16453528,"avatar_url":"https://avatars.githubusercontent.com/u/16453528?v=3","gravatar_id":"","url":"https://api.github.com/users/portier","html_url":"https://github.com/portier","followers_url":"https://api.github.com/users/portier/followers","following_url":"https://api.github.com/users/portier/following{/other_user}","gists_url":"https://api.github.com/users/portier/gists{/gist_id}","starred_url":"https://api.github.com/users/portier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/portier/subscriptions","organizations_url":"https://api.github.com/users/portier/orgs","repos_url":"https://api.github.com/users/portier/repos","events_url":"https://api.github.com/users/portier/events{/privacy}","received_events_url":"https://api.github.com/users/portier/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/portier/portier-broker","description":"Portier Broker reference implementation, written in Rust","fork":false,"url":"https://api.github.com/repos/portier/portier-broker","forks_url":"https://api.github.com/repos/portier/portier-broker/forks","keys_url":"https://api.github.com/repos/portier/portier-broker/keys{/key_id}","collaborators_url":"https://api.github.com/repos/portier/portier-broker/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/portier/portier-broker/teams","hooks_url":"https://api.github.com/repos/portier/portier-broker/hooks","issue_events_url":"https://api.github.com/repos/portier/portier-broker/issues/events{/number}","events_url":"https://api.github.com/repos/portier/portier-broker/events","assignees_url":"https://api.github.com/repos/portier/portier-broker/assignees{/user}","branches_url":"https://api.github.com/repos/portier/portier-broker/branches{/branch}","tags_url":"https://api.github.com/repos/portier/portier-broker/tags","blobs_url":"https://api.github.com/repos/portier/portier-broker/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/portier/portier-broker/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/portier/portier-broker/git/refs{/sha}","trees_url":"https://api.github.com/repos/portier/portier-broker/git/trees{/sha}","statuses_url":"https://api.github.com/repos/portier/portier-broker/statuses/{sha}","languages_url":"https://api.github.com/repos/portier/portier-broker/languages","stargazers_url":"https://api.github.com/repos/portier/portier-broker/stargazers","contributors_url":"https://api.github.com/repos/portier/portier-broker/contributors","subscribers_url":"https://api.github.com/repos/portier/portier-broker/subscribers","subscription_url":"https://api.github.com/repos/portier/portier-broker/subscription","commits_url":"https://api.github.com/repos/portier/portier-broker/commits{/sha}","git_commits_url":"https://api.github.com/repos/portier/portier-broker/git/commits{/sha}","comments_url":"https://api.github.com/repos/portier/portier-broker/comments{/number}","issue_comment_url":"https://api.github.com/repos/portier/portier-broker/issues/comments{/number}","contents_url":"https://api.github.com/repos/portier/portier-broker/contents/{+path}","compare_url":"https://api.github.com/repos/portier/portier-broker/compare/{base}...{head}","merges_url":"https://api.github.com/repos/portier/portier-broker/merges","archive_url":"https://api.github.com/repos/portier/portier-broker/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/portier/portier-broker/downloads","issues_url":"https://api.github.com/repos/portier/portier-broker/issues{/number}","pulls_url":"https://api.github.com/repos/portier/portier-broker/pulls{/number}","milestones_url":"https://api.github.com/repos/portier/portier-broker/milestones{/number}","notifications_url":"https://api.github.com/repos/portier/portier-broker/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/portier/portier-broker/labels{/name}","releases_url":"https://api.github.com/repos/portier/portier-broker/releases{/id}","deployments_url":"https://api.github.com/repos/portier/portier-broker/deployments","created_at":"2016-05-18T20:10:35Z","updated_at":"2016-11-21T08:41:41Z","pushed_at":"2016-11-22T07:58:04Z","git_url":"git://github.com/portier/portier-broker.git","ssh_url":"git@github.com:portier/portier-broker.git","clone_url":"https://github.com/portier/portier-broker.git","svn_url":"https://github.com/portier/portier-broker","homepage":"http://portier.github.io/","size":401,"stargazers_count":224,"watchers_count":224,"language":"Rust","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":3,"mirror_url":null,"open_issues_count":24,"forks":3,"open_issues":24,"watchers":224,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/portier/portier-broker/pulls/109"},"html":{"href":"https://github.com/portier/portier-broker/pull/109"},"issue":{"href":"https://api.github.com/repos/portier/portier-broker/issues/109"},"comments":{"href":"https://api.github.com/repos/portier/portier-broker/issues/109/comments"},"review_comments":{"href":"https://api.github.com/repos/portier/portier-broker/pulls/109/comments"},"review_comment":{"href":"https://api.github.com/repos/portier/portier-broker/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/portier/portier-broker/pulls/109/commits"},"statuses":{"href":"https://api.github.com/repos/portier/portier-broker/statuses/ae1e0abca599ee94bfc69f7227ecfe1bbc93f8a6"}}}}
{"actor":{"display_login":"stephank"}}