From c148a5e6cc0055693654003aa04d6fdda2cb035e Mon Sep 17 00:00:00 2001 From: Keshav Kk Date: Tue, 23 Sep 2025 13:45:41 +0900 Subject: [PATCH 1/6] Gemspec Metadata --- actionwebpush.gemspec | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/actionwebpush.gemspec b/actionwebpush.gemspec index 6a4522b..3753b1a 100644 --- a/actionwebpush.gemspec +++ b/actionwebpush.gemspec @@ -19,6 +19,10 @@ Gem::Specification.new do |spec| spec.metadata["homepage_uri"] = spec.homepage spec.metadata["source_code_uri"] = "https://github.com/keshav-k3/actionwebpush" spec.metadata["changelog_uri"] = "https://github.com/keshav-k3/actionwebpush/blob/main/CHANGELOG.md" + spec.metadata["bug_tracker_uri"] = "https://github.com/keshav-k3/actionwebpush/issues" + spec.metadata["documentation_uri"] = "https://github.com/keshav-k3/actionwebpush#readme" + spec.metadata["wiki_uri"] = "https://github.com/keshav-k3/actionwebpush/wiki" + spec.metadata["rubygems_mfa_required"] = "true" # Specify which files should be added to the gem when it is released. # The `git ls-files -z` loads the files in the RubyGem that have been added into git. @@ -35,14 +39,14 @@ Gem::Specification.new do |spec| spec.require_paths = ["lib"] # Runtime dependencies - spec.add_dependency "rails", ">= 6.0" - spec.add_dependency "web-push", "~> 3.0" - spec.add_dependency "concurrent-ruby", "~> 1.1" - spec.add_dependency "net-http-persistent", "~> 4.0" + spec.add_dependency "rails", ">= 6.0", "< 9.0" + spec.add_dependency "web-push", "~> 3.0", ">= 3.0.0" + spec.add_dependency "concurrent-ruby", "~> 1.1", ">= 1.1.0" + spec.add_dependency "net-http-persistent", "~> 4.0", ">= 4.0.0" # Development dependencies - spec.add_development_dependency "sqlite3", "~> 1.4" - spec.add_development_dependency "resque", "~> 2.0" + spec.add_development_dependency "sqlite3", "~> 1.4", ">= 1.4.0" + spec.add_development_dependency "resque", "~> 2.0", ">= 2.0.0" # For more information and examples about making a new gem, check out our # guide at: https://bundler.io/guides/creating_gem.html From 5f07eaa20e8231b7638507c7d730c6526d86756c Mon Sep 17 00:00:00 2001 From: Keshav Kk Date: Tue, 23 Sep 2025 13:47:58 +0900 Subject: [PATCH 2/6] Centralized ActionWebPush::Logging module --- lib/actionwebpush.rb | 1 + lib/actionwebpush/batch_delivery.rb | 4 +-- lib/actionwebpush/delivery_methods/base.rb | 11 ++----- lib/actionwebpush/logging.rb | 38 ++++++++++++++++++++++ lib/actionwebpush/pool.rb | 4 +-- 5 files changed, 43 insertions(+), 15 deletions(-) create mode 100644 lib/actionwebpush/logging.rb diff --git a/lib/actionwebpush.rb b/lib/actionwebpush.rb index 9d93c87..2b3cc64 100644 --- a/lib/actionwebpush.rb +++ b/lib/actionwebpush.rb @@ -24,6 +24,7 @@ class RateLimitExceeded < Error; end autoload :TenantManager, "actionwebpush/tenant_configuration" autoload :SentryIntegration, "actionwebpush/sentry_integration" autoload :Analytics, "actionwebpush/analytics" + autoload :Logging, "actionwebpush/logging" module DeliveryMethods autoload :Base, "actionwebpush/delivery_methods/base" diff --git a/lib/actionwebpush/batch_delivery.rb b/lib/actionwebpush/batch_delivery.rb index e26f7f4..fe3c145 100644 --- a/lib/actionwebpush/batch_delivery.rb +++ b/lib/actionwebpush/batch_delivery.rb @@ -2,6 +2,7 @@ module ActionWebPush class BatchDelivery + include ActionWebPush::Logging attr_reader :notifications, :pool def initialize(notifications, pool: nil) @@ -59,8 +60,5 @@ def handle_expired_subscription(notification) logger.warn "Failed to cleanup expired subscription: #{e.message}" end - def logger - ActionWebPush.config.logger || (defined?(Rails) ? Rails.logger : Logger.new(STDOUT)) - end end end \ No newline at end of file diff --git a/lib/actionwebpush/delivery_methods/base.rb b/lib/actionwebpush/delivery_methods/base.rb index 8010ad8..4d09a6c 100644 --- a/lib/actionwebpush/delivery_methods/base.rb +++ b/lib/actionwebpush/delivery_methods/base.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -require "logger" - module ActionWebPush module DeliveryMethods class Base + include ActionWebPush::Logging + attr_reader :settings def initialize(settings = {}) @@ -14,13 +14,6 @@ def initialize(settings = {}) def deliver!(notification, connection: nil) raise NotImplementedError, "Subclasses must implement deliver!" end - - protected - - def logger - ActionWebPush.config.logger || - (defined?(Rails) && Rails.respond_to?(:logger) ? Rails.logger : Logger.new(STDOUT)) - end end end end \ No newline at end of file diff --git a/lib/actionwebpush/logging.rb b/lib/actionwebpush/logging.rb new file mode 100644 index 0000000..02c9933 --- /dev/null +++ b/lib/actionwebpush/logging.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require "logger" + +module ActionWebPush + module Logging + def logger + ActionWebPush.logger + end + + module_function :logger + end + + class << self + def logger + @logger ||= config.logger || default_logger + end + + def logger=(logger) + @logger = logger + end + + private + + def default_logger + if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger + Rails.logger + else + Logger.new(STDOUT).tap do |log| + log.level = Logger::INFO + log.formatter = proc do |severity, datetime, progname, msg| + "[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity} -- ActionWebPush: #{msg}\n" + end + end + end + end + end +end \ No newline at end of file diff --git a/lib/actionwebpush/pool.rb b/lib/actionwebpush/pool.rb index ecac810..6aaf694 100644 --- a/lib/actionwebpush/pool.rb +++ b/lib/actionwebpush/pool.rb @@ -5,6 +5,7 @@ module ActionWebPush class Pool + include ActionWebPush::Logging attr_reader :delivery_pool, :invalidation_pool, :connection, :invalid_subscription_handler def initialize(invalid_subscription_handler: nil) @@ -72,8 +73,5 @@ def shutdown_pool(pool) pool.kill unless pool.wait_for_termination(1) end - def logger - ActionWebPush.config.logger || (defined?(Rails) ? Rails.logger : Logger.new(STDOUT)) - end end end \ No newline at end of file From 319081f5f6127016abea059926926ec2685d403b Mon Sep 17 00:00:00 2001 From: Keshav Kk Date: Tue, 23 Sep 2025 13:57:19 +0900 Subject: [PATCH 3/6] Added configurable batch processing --- lib/actionwebpush/batch_delivery.rb | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/actionwebpush/batch_delivery.rb b/lib/actionwebpush/batch_delivery.rb index fe3c145..70cd064 100644 --- a/lib/actionwebpush/batch_delivery.rb +++ b/lib/actionwebpush/batch_delivery.rb @@ -3,18 +3,22 @@ module ActionWebPush class BatchDelivery include ActionWebPush::Logging - attr_reader :notifications, :pool + attr_reader :notifications, :pool, :batch_size - def initialize(notifications, pool: nil) + def initialize(notifications, pool: nil, batch_size: nil) @notifications = Array(notifications) @pool = pool || (defined?(Rails) ? Rails.configuration.x.action_web_push_pool : nil) + @batch_size = batch_size || ActionWebPush.config.batch_size || 100 end def deliver_all - if pool - batch_deliver_with_pool - else - direct_batch_deliver + # Process notifications in batches to avoid overwhelming the system + notifications.each_slice(batch_size) do |batch| + if pool + batch_deliver_with_pool(batch) + else + direct_batch_deliver(batch) + end end end @@ -24,9 +28,9 @@ def self.deliver(notifications, **options) private - def batch_deliver_with_pool + def batch_deliver_with_pool(batch_notifications) # Group notifications by endpoint to avoid overwhelming single endpoints - grouped = notifications.group_by(&:endpoint) + grouped = batch_notifications.group_by(&:endpoint) grouped.each do |endpoint, endpoint_notifications| # Stagger delivery to same endpoint to avoid rate limiting @@ -39,8 +43,8 @@ def batch_deliver_with_pool end end - def direct_batch_deliver - notifications.each { |notification| deliver_single(notification) } + def direct_batch_deliver(batch_notifications) + batch_notifications.each { |notification| deliver_single(notification) } end def deliver_single(notification) From 66f91c2654ddc0fd905fe7b418d09388355257eb Mon Sep 17 00:00:00 2001 From: Keshav Kk Date: Tue, 23 Sep 2025 13:57:50 +0900 Subject: [PATCH 4/6] Added configurable batch processing conf --- lib/actionwebpush/configuration.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/actionwebpush/configuration.rb b/lib/actionwebpush/configuration.rb index a4b8290..695920d 100644 --- a/lib/actionwebpush/configuration.rb +++ b/lib/actionwebpush/configuration.rb @@ -3,7 +3,7 @@ module ActionWebPush class Configuration attr_accessor :vapid_public_key, :vapid_private_key, :vapid_subject - attr_accessor :pool_size, :queue_size, :delivery_method, :connection_pool_size + attr_accessor :pool_size, :queue_size, :delivery_method, :connection_pool_size, :batch_size attr_accessor :logger, :timeout, :max_retries, :async attr_reader :delivery_methods @@ -11,6 +11,7 @@ def initialize @pool_size = 50 @queue_size = 10000 @connection_pool_size = 150 + @batch_size = 100 @delivery_method = :web_push @vapid_subject = "mailto:support@example.com" @logger = nil From 556699fab69721b56b2accedf3c67aeb9d55dcaa Mon Sep 17 00:00:00 2001 From: Keshav Kk Date: Tue, 23 Sep 2025 13:57:58 +0900 Subject: [PATCH 5/6] Subscription Management --- app/models/actionwebpush/subscription.rb | 66 ++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/app/models/actionwebpush/subscription.rb b/app/models/actionwebpush/subscription.rb index 54e1377..5b3781b 100644 --- a/app/models/actionwebpush/subscription.rb +++ b/app/models/actionwebpush/subscription.rb @@ -2,6 +2,8 @@ module ActionWebPush class Subscription < ActiveRecord::Base + include ActionWebPush::Logging + self.table_name = "action_web_push_subscriptions" belongs_to :user @@ -11,6 +13,11 @@ class Subscription < ActiveRecord::Base validates :auth_key, presence: true validates :endpoint, uniqueness: { scope: [:p256dh_key, :auth_key] } + # Lifecycle callbacks + before_create :log_subscription_creation + before_destroy :log_subscription_destruction + after_touch :log_subscription_activity + scope :for_user, ->(user) { where(user: user) } scope :active, -> { where("updated_at > ?", 30.days.ago) } scope :stale, -> { where("updated_at <= ?", 30.days.ago) } @@ -51,9 +58,17 @@ def self.find_or_create_subscription(user:, endpoint:, p256dh_key:, auth_key:, * end end - def self.cleanup_stale_subscriptions! - count = stale.delete_all - Rails.logger.info "ActionWebPush cleaned up #{count} stale subscriptions" if defined?(Rails) + def self.cleanup_stale_subscriptions!(dry_run: false) + stale_subscriptions = stale + count = stale_subscriptions.count + + if dry_run + ActionWebPush.logger.info "ActionWebPush would cleanup #{count} stale subscriptions (dry run)" + return count + end + + stale_subscriptions.delete_all + ActionWebPush.logger.info "ActionWebPush cleaned up #{count} stale subscriptions" count end @@ -74,9 +89,54 @@ def stale? def test_delivery!(title: "Test Notification", body: "This is a test push notification") notification = build_notification(title: title, body: body) notification.deliver_now + touch # Update last activity true rescue ActionWebPush::Error => e + logger.warn "Test delivery failed for subscription #{id}: #{e.message}" false end + + def mark_as_expired! + logger.info "Marking subscription #{id} as expired" + destroy + end + + def refresh_activity! + touch + logger.debug "Refreshed activity for subscription #{id}" + end + + def endpoint_domain + URI.parse(endpoint).host + rescue URI::InvalidURIError + nil + end + + def days_since_last_activity + ((Time.current - updated_at) / 1.day).round + end + + def self.stats + { + total: count, + active: active.count, + stale: stale.count, + by_domain: group("SUBSTRING(endpoint FROM 'https?://([^/]+)')").count + } + end + + private + + def log_subscription_creation + logger.info "Creating push subscription for user #{user_id} on #{endpoint_domain}" + end + + def log_subscription_destruction + logger.info "Destroying push subscription #{id} for user #{user_id}" + end + + def log_subscription_activity + logger.debug "Push subscription #{id} activity updated" + end end end \ No newline at end of file From f24aa95778183c881e88735655b8d6a0da62ce53 Mon Sep 17 00:00:00 2001 From: Keshav Kk Date: Tue, 23 Sep 2025 13:58:15 +0900 Subject: [PATCH 6/6] Memory Leak in Rate Limiter --- lib/actionwebpush/rate_limiter.rb | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/lib/actionwebpush/rate_limiter.rb b/lib/actionwebpush/rate_limiter.rb index f84cc7f..b7979f5 100644 --- a/lib/actionwebpush/rate_limiter.rb +++ b/lib/actionwebpush/rate_limiter.rb @@ -5,13 +5,19 @@ module ActionWebPush class RateLimiter class MemoryStore + CLEANUP_INTERVAL = 300 # 5 minutes + def initialize @store = {} @mutex = Mutex.new + @last_cleanup = Time.current end def increment(key, ttl) @mutex.synchronize do + # Periodic automatic cleanup + auto_cleanup! if should_cleanup? + @store[key] ||= { count: 0, expires_at: Time.current + ttl } if @store[key][:expires_at] < Time.current @@ -26,7 +32,29 @@ def increment(key, ttl) def cleanup! @mutex.synchronize do - @store.reject! { |_, v| v[:expires_at] < Time.current } + auto_cleanup! + end + end + + def size + @mutex.synchronize { @store.size } + end + + private + + def should_cleanup? + Time.current - @last_cleanup > CLEANUP_INTERVAL + end + + def auto_cleanup! + before_count = @store.size + @store.reject! { |_, v| v[:expires_at] < Time.current } + @last_cleanup = Time.current + + # Log significant cleanups + cleaned = before_count - @store.size + if cleaned > 0 && defined?(ActionWebPush) && ActionWebPush.respond_to?(:logger) + ActionWebPush.logger.debug "ActionWebPush::RateLimiter cleaned up #{cleaned} expired entries (#{@store.size} remaining)" end end end