Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(flags++): added quota limiting to the new flags service and in doing so refactored out a bunch of common stuff #29228

Open
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

dmarticus
Copy link
Contributor

Problem

TODO – wanna see if this passes CI first.

@dmarticus dmarticus changed the title feature(flags++): added quota limiting to the new flags service and in doing so refactored out a bunch of common stuff feat(flags++): added quota limiting to the new flags service and in doing so refactored out a bunch of common stuff Feb 26, 2025
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Summary

This PR refactors the codebase to implement quota limiting in the feature flags service by extracting common functionality into shared modules.

  • Moved Redis client implementation from service-specific modules to rust/common/redis for consistent usage across services
  • Relocated rate limiting functionality from capture service to rust/common/limiters with three submodules: overflow, redis, and token_dropper
  • Added support for feature flag request limiting by introducing a FeatureFlags variant to the QuotaResource enum
  • Updated import paths across multiple files to use the new common modules instead of service-specific implementations
  • Integrated the shared RedisLimiter into the feature flags service router and server components for quota management

30 file(s) reviewed, 8 comment(s)
Edit PR Review Bot Settings | Greptile

common-redis = { path = "../redis" }
governor = { workspace = true }
metrics = { workspace = true }
rand = { workspace = true}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: There's an extra space after rand = { workspace = true} between 'true' and the closing brace. This should be rand = { workspace = true } for consistency with other workspace dependencies.

Suggested change
rand = { workspace = true}
rand = { workspace = true }

"cluster",
"cluster-async",
] }
serde-pickle = { version = "1.1.1"}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: The serde-pickle dependency is missing a space between the version and closing brace, unlike the other dependencies. This is a minor inconsistency in formatting.

] }
serde-pickle = { version = "1.1.1"}
thiserror = { workspace = true }
tokio = { workspace = true }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: The file is missing a newline at the end, which is typically recommended for text files in Unix-like systems.

Suggested change
tokio = { workspace = true }
tokio = { workspace = true }

/// Implements functionality from both feature-flags and capture Redis clients
#[async_trait]
pub trait Client {
async fn zrangebyscore(&self, k: String, min: String, max: String) -> Result<Vec<String>>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: The return type for zrangebyscore is inconsistent with other methods - it uses anyhow::Result while other methods use Result<T, CustomRedisError>. This inconsistency could lead to confusion and different error handling patterns.

Suggested change
async fn zrangebyscore(&self, k: String, min: String, max: String) -> Result<Vec<String>>;
async fn zrangebyscore(&self, k: String, min: String, max: String) -> Result<Vec<String>, CustomRedisError>;

Comment on lines +101 to +106
if match &fut {
Ok(v) => v.is_empty(),
Err(_) => false,
} {
return Err(CustomRedisError::NotFound);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: This error handling logic for empty values is only applied to the get method, but not to other methods like hget. Consider applying consistent error handling for empty values across all methods.

Comment on lines 202 to 206
async fn zrangebyscore(&self, key: String, _min: String, _max: String) -> Result<Vec<String>> {
match self.zrangebyscore_ret.get(&key) {
Some(val) => Ok(val.clone()),
None => Err(anyhow::anyhow!("Not found")),
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: The mock implementation of zrangebyscore returns anyhow::Error with a string message, while the real implementation returns various Redis errors. This inconsistency could cause tests to pass that would fail in production.

Suggested change
async fn zrangebyscore(&self, key: String, _min: String, _max: String) -> Result<Vec<String>> {
match self.zrangebyscore_ret.get(&key) {
Some(val) => Ok(val.clone()),
None => Err(anyhow::anyhow!("Not found")),
}
async fn zrangebyscore(&self, key: String, _min: String, _max: String) -> Result<Vec<String>> {
match self.zrangebyscore_ret.get(&key) {
Some(val) => Ok(val.clone()),
None => Err(CustomRedisError::NotFound.into()),
}

Comment on lines +228 to +234
async fn set(&self, key: String, _value: String) -> Result<(), CustomRedisError> {
match self.set_ret.get(&key) {
Some(result) => result
.clone()
.map_err(|e| CustomRedisError::Other(e.to_string())),
None => Err(CustomRedisError::NotFound),
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: The mock set implementation ignores the value parameter, which means tests won't catch bugs related to serialization of specific values.

Comment on lines 88 to 95
let billing_limiter = RedisLimiter::new(
Duration::seconds(5),
redis_client.clone(),
QUOTA_LIMITER_CACHE_KEY.to_string(),
None,
QuotaResource::FeatureFlags,
)
.expect("failed to create billing limiter");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Using .expect() here will cause the service to panic if Redis is unavailable. Consider handling this error more gracefully, similar to how other client initialization errors are handled above.

Suggested change
let billing_limiter = RedisLimiter::new(
Duration::seconds(5),
redis_client.clone(),
QUOTA_LIMITER_CACHE_KEY.to_string(),
None,
QuotaResource::FeatureFlags,
)
.expect("failed to create billing limiter");
let billing_limiter = match RedisLimiter::new(
Duration::seconds(5),
redis_client.clone(),
QUOTA_LIMITER_CACHE_KEY.to_string(),
None,
QuotaResource::FeatureFlags,
) {
Ok(limiter) => limiter,
Err(e) => {
tracing::error!("Failed to create billing limiter: {}", e);
return;
}
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant