Module: Familia::Features::Quantization
- Defined in:
- lib/familia/features/quantization.rb
Overview
Quantization is a feature that provides time-based data bucketing and quantized timestamp generation for Familia objects. It enables efficient time-series data storage, analytics aggregation, and temporal cache key generation by rounding timestamps to specific intervals.
This feature is particularly useful for:
- Time-series data collection and storage
- Analytics data bucketing by time intervals
- Cache key generation with time-based expiration
- Log aggregation by time periods
- Metrics collection with reduced granularity
- Rate limiting with time windows
Example:
class AnalyticsEvent < Familia::Horreum feature :quantization default_expiration 1.hour # Used as default quantum
identifier_field :event_id
field :event_id, :event_type, :user_id, :data, :timestamp
end
# Generate quantized timestamps AnalyticsEvent.qstamp(1.hour) # => 1672531200 (rounded to hour) AnalyticsEvent.qstamp(1.hour, '%Y%m%d%H') # => "2023010114" (formatted) AnalyticsEvent.qstamp([1.hour, '%Y%m%d%H']) # => "2023010114" (array syntax)
# Instance method also available event = AnalyticsEvent.new event.qstamp(15.minutes) # => 1672531800 (15-min buckets)
Time Bucketing:
Quantization rounds timestamps to specific intervals, creating consistent time buckets for data aggregation:
# Current time: 2023-01-01 14:37:42 User.qstamp(1.hour) # => 1672531200 (14:00:00) User.qstamp(15.minutes) # => 1672532100 (14:35:00) User.qstamp(1.day) # => 1672531200 (00:00:00)
Formatted Timestamps:
Use strftime patterns to generate formatted timestamp strings:
User.qstamp(1.hour, pattern: '%Y%m%d%H') # => "2023010114" User.qstamp(1.day, pattern: '%Y-%m-%d') # => "2023-01-01" User.qstamp(1.week, pattern: '%Y-W%W') # => "2023-W01"
Custom Time Reference:
Specify a custom time instead of using the current time:
custom_time = Time.parse('2023-06-15 14:30:45') User.qstamp(1.hour, time: custom_time) # => 1686834000 (14:00:00) User.qstamp(1.day, time: custom_time, pattern: '%Y%m%d') # => "20230615"
Integration Patterns:
# Time-based cache keys class MetricsCache < Familia::Horreum feature :quantization identifier_field :cache_key
field :cache_key, :data, :computed_at
hashkey :hourly_metrics
def self.hourly_cache_key(metric_name)
= qstamp(1.hour, pattern: '%Y%m%d%H')
"metrics:#{metric_name}:#{}"
end
def self.daily_cache_key(metric_name)
= qstamp(1.day, pattern: '%Y%m%d')
"daily_metrics:#{metric_name}:#{}"
end
end
# Usage hourly_key = MetricsCache.hourly_cache_key('page_views') # => "metrics:page_views:2023010114"
# Analytics data bucketing class UserActivity < Familia::Horreum feature :quantization identifier_field :bucket_id
field :bucket_id, :user_count, :event_count, :bucket_time
def self.record_activity(user_id, event_type)
# Create hourly buckets
bucket_time = qstamp(1.hour)
bucket_id = "activity:#{qstamp(1.hour, pattern: '%Y%m%d%H')}"
activity = find_or_create(bucket_id) do
new(bucket_id: bucket_id, bucket_time: bucket_time,
user_count: 0, event_count: 0)
end
activity.event_count += 1
activity.save
end
def self.activity_for_hour(time = Familia.now)
bucket_id = "activity:#{qstamp(1.hour, time: time, pattern: '%Y%m%d%H')}"
find(bucket_id)
end
end
# Time-series data storage class TimeSeriesMetric < Familia::Horreum feature :quantization identifier_field :series_key
field :series_key, :metric_name, :interval, :value, :timestamp
zset :data_points # score = timestamp, member = value
def self.record_metric(metric_name, value, interval = 5.minutes)
= qstamp(interval)
series_key = "#{metric_name}:#{interval.to_i}"
metric = find_or_create(series_key) do
new(series_key: series_key, metric_name: metric_name,
interval: interval.to_i)
end
metric.data_points.add(value, )
metric. =
metric.value = value
metric.save
end
def self.get_series(metric_name, interval, start_time, end_time)
series_key = "#{metric_name}:#{interval.to_i}"
metric = find(series_key)
return [] unless metric
start_bucket = qstamp(interval, time: start_time)
end_bucket = qstamp(interval, time: end_time)
metric.data_points.range_by_score(start_bucket, end_bucket, with_scores: true)
end
end
Quantum Calculation:
The quantum (time interval) determines the bucket size:
- 1.minute: Buckets every minute (00, 01, 02, ...)
- 5.minutes: Buckets every 5 minutes (00, 05, 10, 15, ...)
- 1.hour: Buckets every hour (00:00, 01:00, 02:00, ...)
- 1.day: Daily buckets (00:00:00 each day)
- 1.week: Weekly buckets (start of week)
Understanding Quantum Boundaries:
# Current time: 2023-01-01 14:37:42
# 1.hour quantum (rounds down to hour boundary) qstamp(1.hour) # => 1672531200 (2023-01-01 14:00:00)
# 15.minutes quantum (rounds down to 15-minute boundary) qstamp(15.minutes) # => 1672532100 (2023-01-01 14:30:00)
# 1.day quantum (rounds down to day boundary) qstamp(1.day) # => 1672531200 (2023-01-01 00:00:00)
Cross-Timezone Considerations:
class GlobalMetrics < Familia::Horreum feature :quantization
def self.utc_hourly_key(metric_name)
# Always use UTC for consistent global buckets
= qstamp(1.hour, time: Familia.now, pattern: '%Y%m%d%H')
"global:#{metric_name}:#{}"
end
def self.local_daily_key(metric_name, timezone = 'America/New_York')
# Use local timezone for region-specific buckets
local_time = Familia.now.in_time_zone(timezone)
= qstamp(1.day, time: local_time, pattern: '%Y%m%d')
"#{timezone.gsub('/', '_')}:#{metric_name}:#{}"
end
end
Performance Optimization:
class OptimizedQuantization < Familia::Horreum feature :quantization
# Cache quantized timestamps to avoid repeated calculations
def self.cached_qstamp(quantum, pattern: nil, time: nil)
cache_key = "qstamp:#{quantum}:#{pattern}:#{(time || Familia.now).to_i / quantum}"
Rails.cache.fetch(cache_key, expires_in: quantum) do
qstamp(quantum, pattern: pattern, time: time)
end
end
# Batch quantize multiple timestamps
def self.batch_quantize(, quantum)
.map { |ts| Familia.qstamp(quantum, time: ts) }
end
# Pre-generate bucket timestamps for a time range
def self.pregenerate_buckets(start_time, end_time, quantum)
buckets = []
current = Familia.qstamp(quantum, time: start_time)
end_bucket = Familia.qstamp(quantum, time: end_time)
while current <= end_bucket
buckets << current
current += quantum
end
buckets
end
end
Error Handling:
The feature validates quantum values and provides descriptive errors:
User.qstamp(0) # => ArgumentError: Quantum must be positive User.qstamp(-5) # => ArgumentError: Quantum must be positive User.qstamp("invalid") # => ArgumentError: Quantum must be positive
Default Quantum Behavior:
If no quantum is specified, the feature uses default_expiration or 10.minutes:
class MyModel < Familia::Horreum feature :quantization default_expiration 1.hour end
MyModel.qstamp() # Uses 1.hour as quantum
class NoDefault < Familia::Horreum feature :quantization end
NoDefault.qstamp() # Uses 10.minutes as fallback quantum
Defined Under Namespace
Modules: ModelClassMethods
Instance Method Summary collapse
-
#qstamp(quantum = nil, pattern: nil, time: nil) ⇒ Integer, String
Instance method version of qstamp.
-
#quantized_identifier(quantum, pattern: nil, separator: Familia.delim) ⇒ String
Generate a quantized identifier for this instance.
Instance Method Details
#qstamp(quantum = nil, pattern: nil, time: nil) ⇒ Integer, String
Instance method version of qstamp
Generates a quantized timestamp using the same logic as the class method, but can access instance-specific default expiration settings.
372 373 374 375 376 |
# File 'lib/familia/features/quantization.rb', line 372 def qstamp(quantum = nil, pattern: nil, time: nil) # Use instance default_expiration if available, otherwise delegate to class quantum ||= default_expiration if respond_to?(:default_expiration) self.class.qstamp(quantum, pattern: pattern, time: time) end |
#quantized_identifier(quantum, pattern: nil, separator: Familia.delim) ⇒ String
Generate a quantized identifier for this instance
Creates a time-based identifier using the instance's identifier and a quantized timestamp. Useful for creating time-bucketed cache keys or grouping identifiers.
394 395 396 397 398 |
# File 'lib/familia/features/quantization.rb', line 394 def quantized_identifier(quantum, pattern: nil, separator: Familia.delim) = qstamp(quantum, pattern: pattern) base_id = respond_to?(:identifier) ? identifier : object_id "#{base_id}#{separator}#{}" end |