tekeya 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +0 -0
- data/.gitignore +23 -0
- data/.rspec +3 -0
- data/.yardopts +1 -0
- data/Gemfile +39 -0
- data/Guardfile +40 -0
- data/LICENSE +22 -0
- data/README.md +37 -0
- data/Rakefile +13 -0
- data/TODO.todo +8 -0
- data/app/active_record/tekeya/activity.rb +7 -0
- data/app/active_record/tekeya/attachment.rb +5 -0
- data/app/active_record/tekeya/notification.rb +5 -0
- data/app/mongoid/tekeya/activity.rb +9 -0
- data/app/mongoid/tekeya/attachment.rb +6 -0
- data/app/mongoid/tekeya/notification.rb +10 -0
- data/db/migrate/00_create_activities.rb +11 -0
- data/db/migrate/01_create_attachments.rb +13 -0
- data/db/migrate/02_create_notifications.rb +14 -0
- data/lib/tasks/resque_tasks.rake +3 -0
- data/lib/tekeya.rb +81 -0
- data/lib/tekeya/configuration.rb +48 -0
- data/lib/tekeya/entity.rb +432 -0
- data/lib/tekeya/entity/group.rb +22 -0
- data/lib/tekeya/errors/tekeya_error.rb +11 -0
- data/lib/tekeya/errors/tekeya_fatal.rb +11 -0
- data/lib/tekeya/errors/tekeya_non_entity.rb +6 -0
- data/lib/tekeya/errors/tekeya_non_group.rb +6 -0
- data/lib/tekeya/errors/tekeya_relation_already_exists.rb +6 -0
- data/lib/tekeya/errors/tekeya_relation_non_existent.rb +6 -0
- data/lib/tekeya/feed/activity.rb +101 -0
- data/lib/tekeya/feed/activity/feed_item.rb +58 -0
- data/lib/tekeya/feed/activity/resque.rb +56 -0
- data/lib/tekeya/feed/activity/resque/activity_fanout.rb +61 -0
- data/lib/tekeya/feed/activity/resque/delete_activity.rb +43 -0
- data/lib/tekeya/feed/activity/resque/feed_copy.rb +34 -0
- data/lib/tekeya/feed/activity/resque/untrack_feed.rb +34 -0
- data/lib/tekeya/feed/attachable.rb +15 -0
- data/lib/tekeya/feed/attachment.rb +19 -0
- data/lib/tekeya/feed/notification.rb +93 -0
- data/lib/tekeya/railtie.rb +18 -0
- data/lib/tekeya/version.rb +3 -0
- data/spec/fabricators/attachment_fabricator.rb +3 -0
- data/spec/fabricators/group_fabricator.rb +4 -0
- data/spec/fabricators/status_fabricator.rb +3 -0
- data/spec/fabricators/user_fabricator.rb +3 -0
- data/spec/orm/active_record.rb +14 -0
- data/spec/orm/mongoid.rb +6 -0
- data/spec/rails_app/Rakefile +7 -0
- data/spec/rails_app/app/active_record/group.rb +3 -0
- data/spec/rails_app/app/active_record/status.rb +3 -0
- data/spec/rails_app/app/active_record/user.rb +3 -0
- data/spec/rails_app/app/controllers/application_controller.rb +3 -0
- data/spec/rails_app/app/helpers/application_helper.rb +2 -0
- data/spec/rails_app/app/mongoid/group.rb +6 -0
- data/spec/rails_app/app/mongoid/status.rb +6 -0
- data/spec/rails_app/app/mongoid/user.rb +7 -0
- data/spec/rails_app/app/views/layouts/application.html.erb +14 -0
- data/spec/rails_app/config.ru +4 -0
- data/spec/rails_app/config/application.rb +35 -0
- data/spec/rails_app/config/boot.rb +8 -0
- data/spec/rails_app/config/database.yml +21 -0
- data/spec/rails_app/config/environment.rb +5 -0
- data/spec/rails_app/config/environments/development.rb +18 -0
- data/spec/rails_app/config/environments/production.rb +33 -0
- data/spec/rails_app/config/environments/test.rb +33 -0
- data/spec/rails_app/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/rails_app/config/initializers/configure_mongoid.rb +6 -0
- data/spec/rails_app/config/initializers/inflections.rb +15 -0
- data/spec/rails_app/config/initializers/resque.rb +1 -0
- data/spec/rails_app/config/initializers/secret_token.rb +7 -0
- data/spec/rails_app/config/locales/en.yml +5 -0
- data/spec/rails_app/config/routes.rb +58 -0
- data/spec/rails_app/db/migrate/100_create_users.rb +9 -0
- data/spec/rails_app/db/migrate/101_create_groups.rb +11 -0
- data/spec/rails_app/db/migrate/102_create_statuses.rb +9 -0
- data/spec/rails_app/db/seeds.rb +7 -0
- data/spec/rails_app/public/404.html +26 -0
- data/spec/rails_app/public/422.html +26 -0
- data/spec/rails_app/public/500.html +25 -0
- data/spec/rails_app/public/favicon.ico +0 -0
- data/spec/rails_app/public/index.html +241 -0
- data/spec/rails_app/public/robots.txt +5 -0
- data/spec/rails_app/script/rails +6 -0
- data/spec/spec_helper.rb +78 -0
- data/spec/tekeya/activity_spec.rb +170 -0
- data/spec/tekeya/entity_spec.rb +158 -0
- data/spec/tekeya/notification_spec.rb +49 -0
- data/spec/tekeya_helper.rb +7 -0
- data/spec/tekeya_spec.rb +51 -0
- data/tekeya.gemspec +22 -0
- metadata +231 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
module Tekeya
|
2
|
+
module Entity
|
3
|
+
module Group
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
include Entity
|
6
|
+
|
7
|
+
included do
|
8
|
+
belongs_to :owner, polymorphic: true
|
9
|
+
|
10
|
+
validates_presence_of :owner
|
11
|
+
end
|
12
|
+
|
13
|
+
def is_tekeya_group?
|
14
|
+
return true
|
15
|
+
end
|
16
|
+
|
17
|
+
def members(type = nil)
|
18
|
+
tekeya_relations_of(self, :joins, type, true)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Tekeya
|
2
|
+
module Feed
|
3
|
+
module Activity
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
belongs_to :entity, polymorphic: true, autosave: true
|
8
|
+
has_many :attachments, as: :attache, class_name: 'Tekeya::Attachment'
|
9
|
+
|
10
|
+
before_create :group_activities
|
11
|
+
after_create :write_activity_in_redis
|
12
|
+
after_destroy :delete_activity_from_redis
|
13
|
+
|
14
|
+
accepts_nested_attributes_for :attachments
|
15
|
+
|
16
|
+
validates_presence_of :attachments
|
17
|
+
|
18
|
+
attr_writer :group_with_recent
|
19
|
+
end
|
20
|
+
|
21
|
+
# Check if this activity is cached in redis
|
22
|
+
#
|
23
|
+
# @return [Boolean] true if an aggregate of the activity exists in redis, false otherwise
|
24
|
+
def cached_in_redis?
|
25
|
+
::Tekeya.redis.scard(activity_key) > 0
|
26
|
+
end
|
27
|
+
|
28
|
+
# Approximates the timestamp to the nearest 15 minutes for grouping activities
|
29
|
+
#
|
30
|
+
# @param [Datetime] from_time the time to approximate
|
31
|
+
# @return [Integer] the timestamp approximated to the nearest 15 minutes
|
32
|
+
def score(from_time = nil)
|
33
|
+
if from_time.present?
|
34
|
+
stamp = from_time.to_i
|
35
|
+
|
36
|
+
# floors the timestamp to the nearest 15 minute
|
37
|
+
return (stamp.to_f / 15.minutes).floor * 15.minutes
|
38
|
+
else
|
39
|
+
return current_time_from_proper_timezone.to_i
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns an activity key for usage in caching
|
44
|
+
#
|
45
|
+
# @return [String] the activity key
|
46
|
+
def activity_key
|
47
|
+
"activity:#{self.id}:#{self.entity_type}:#{self.entity.send(self.entity.entity_primary_key)}:#{self.activity_type}:#{score}"
|
48
|
+
end
|
49
|
+
|
50
|
+
# @private
|
51
|
+
#
|
52
|
+
# returns if the activity should be grouped with similar recent activities
|
53
|
+
def group_with_recent
|
54
|
+
@group_with_recent.nil? ? true : @group_with_recent
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
# @private
|
60
|
+
# Writes to the activity's aggregate set (a set of attachments associated with the activity)
|
61
|
+
def write_activity_in_redis
|
62
|
+
akey = activity_key
|
63
|
+
tscore = score
|
64
|
+
::Resque.enqueue(::Tekeya::Feed::Activity::Resque::ActivityFanout, self.entity_id, self.entity_type, akey, tscore, self.attachments.map{ |att| att.to_json(root: false, only: [:attachable_id, :attachable_type]) })
|
65
|
+
end
|
66
|
+
|
67
|
+
# @private
|
68
|
+
# Checks if the activity should be grouped and aborts the creation of a new record
|
69
|
+
def group_activities
|
70
|
+
if self.group_with_recent
|
71
|
+
self.created_at = current_time_from_proper_timezone
|
72
|
+
rel = self.class.where(created_at: self.created_at, activity_type: self.activity_type, entity_id: self.entity_id, entity_type: entity_type)
|
73
|
+
if rel.count > 0
|
74
|
+
activity = rel.first
|
75
|
+
activity.attachments << self.attachments
|
76
|
+
self.id = activity.id
|
77
|
+
self.reload
|
78
|
+
return false
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# @private
|
84
|
+
# Deletes the activity's aggregate set when its deleted from the DB
|
85
|
+
def delete_activity_from_redis
|
86
|
+
::Resque.enqueue(::Tekeya::Feed::Activity::Resque::DeleteActivity, self.activity_key)
|
87
|
+
end
|
88
|
+
|
89
|
+
# @private
|
90
|
+
# Override AR's default created_at calculation formula
|
91
|
+
def current_time_from_proper_timezone #:nodoc:
|
92
|
+
zone = self.class.respond_to?(:default_timezone) ? self.class.default_timezone : :utc
|
93
|
+
ctime = zone == :utc ? Time.now.utc : Time.now
|
94
|
+
stamp = ctime.to_i
|
95
|
+
|
96
|
+
# floors the timestamp to the nearest 15 minute
|
97
|
+
return Time.at((stamp.to_f / 15.minutes).floor * 15.minutes)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Tekeya
|
2
|
+
module Feed
|
3
|
+
module Activity
|
4
|
+
class FeedItem
|
5
|
+
attr_reader :activity_id, :activity_type, :attachments, :actor, :timestamp
|
6
|
+
|
7
|
+
def initialize(activity_id, activity_type, attachments, actor, timestamp)
|
8
|
+
@activity_id = activity_id
|
9
|
+
@activity_type = activity_type
|
10
|
+
@attachments = attachments
|
11
|
+
@actor = actor
|
12
|
+
@timestamp = timestamp
|
13
|
+
end
|
14
|
+
|
15
|
+
# Builds a feed item from a redis activity
|
16
|
+
#
|
17
|
+
# @param [String] key the aggregate key of the activity
|
18
|
+
# @param [Tekeya::Entity] act_actor the activty actor; when nil the actor is retrieved from the aggregate key
|
19
|
+
# @return [Tekeya::Feed::FeedItem] the feed item
|
20
|
+
def self.from_redis(key, act_actor = nil)
|
21
|
+
key_components = key.split(':')
|
22
|
+
|
23
|
+
act_id = key_components[1]
|
24
|
+
act_type = key_components[4].to_sym
|
25
|
+
act_time = Time.at(key_components[5].to_i)
|
26
|
+
|
27
|
+
if act_actor.nil?
|
28
|
+
actor_class = key_components[2].safe_constantize
|
29
|
+
act_actor = actor_class.where(:"#{actor_class.entity_primary_key}" => key_components[3]).first
|
30
|
+
end
|
31
|
+
|
32
|
+
act_attachments = ::Tekeya.redis.smembers(key).map{|act|
|
33
|
+
ActiveSupport::JSON.decode(act)
|
34
|
+
}.map{|att|
|
35
|
+
att['attachable_type'].safe_constantize.find att['attachable_id']
|
36
|
+
}
|
37
|
+
|
38
|
+
return self.new(act_id, act_type, act_attachments, act_actor, act_time)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Builds a feed item a DB activity
|
42
|
+
#
|
43
|
+
# @param [Tekeya::Activity] activity the source activity
|
44
|
+
# @param [Tekeya::Entity] act_actor the activty actor; when nil the actor is retrieved from the activity
|
45
|
+
# @return [Tekeya::Feed::FeedItem] the feed item
|
46
|
+
def self.from_db(activity, act_actor = nil)
|
47
|
+
act_id = activity.id.to_s
|
48
|
+
act_type = activity.activity_type.to_sym
|
49
|
+
act_time = activity.created_at
|
50
|
+
act_actor ||= activity.entity
|
51
|
+
act_attachments = activity.attachments.map(&:attachable)
|
52
|
+
|
53
|
+
return self.new(act_id, act_type, act_attachments, act_actor, act_time)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Tekeya
|
2
|
+
module Feed
|
3
|
+
module Activity
|
4
|
+
module Resque
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
MAXTIMESTAMP = 10.days.ago.to_i unless defined?(MAXTIMESTAMP)
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
private
|
13
|
+
# Writes the activity reference to the feed with the supplied key
|
14
|
+
#
|
15
|
+
# @param [String] feed_key the key of the feed where the activity will be referenced
|
16
|
+
# @param [Integer] score the score of the activity (timestamp) used to order the feed
|
17
|
+
# @param [String] activity_key a string containing the key to reference the activity
|
18
|
+
def write_to_feed(feed_key, score, activity_key)
|
19
|
+
# add the activity to the owner's profile feed
|
20
|
+
::Tekeya.redis.zadd(feed_key, score, activity_key)
|
21
|
+
# increment the activity counter to keep track of its presence in feeds
|
22
|
+
activity_counter_key = "#{activity_key}:counter"
|
23
|
+
::Tekeya.redis.incr(activity_counter_key)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Trims the feed according to the MAXTIMESTAMP set and returns the removed keys (for garbage collection)
|
27
|
+
#
|
28
|
+
# @param [String] feed_key a string containing the key of the feed to be trimed
|
29
|
+
def trim_feed(feed_key)
|
30
|
+
removed_keys = ::Tekeya.redis.zrevrangebyscore(feed_key, '-inf', MAXTIMESTAMP)
|
31
|
+
::Tekeya.redis.zremrangebyscore(feed_key, '-inf', MAXTIMESTAMP)
|
32
|
+
|
33
|
+
return removed_keys
|
34
|
+
end
|
35
|
+
|
36
|
+
# Checks if the given keys are referenced in any feed otherwise removes the activity
|
37
|
+
#
|
38
|
+
# @param [Array] keys an array of activity keys to be removed
|
39
|
+
def collect_garbage(keys)
|
40
|
+
keys.each do |key|
|
41
|
+
activity_counter_key = "#{key}:counter"
|
42
|
+
# Check if the key is referenced anywhere
|
43
|
+
if ::Tekeya.redis.get(activity_counter_key).to_i <= 0
|
44
|
+
# Delete the activity and the counter
|
45
|
+
::Tekeya.redis.multi do
|
46
|
+
::Tekeya.redis.del(key)
|
47
|
+
::Tekeya.redis.del(activity_counter_key)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Tekeya
|
2
|
+
module Feed
|
3
|
+
module Activity
|
4
|
+
module Resque
|
5
|
+
# A resque worker to perform the activity fanout operation
|
6
|
+
class ActivityFanout
|
7
|
+
include Tekeya::Feed::Activity::Resque
|
8
|
+
|
9
|
+
@queue = :activity_queue
|
10
|
+
|
11
|
+
# @private
|
12
|
+
def self.perform(entity_id, entity_type, activity_key, score, attachments)
|
13
|
+
# get the entity class
|
14
|
+
entity_type = entity_type.safe_constantize
|
15
|
+
entity = entity_type.where(entity_type.entity_primary_key.to_sym => entity_id).first
|
16
|
+
# we only need the feed keys of the trackers
|
17
|
+
entity_trackers_feeds = entity.trackers.map(&:feed_key)
|
18
|
+
# keep track of the keys we delete in the trim operation for garbage collection
|
19
|
+
removed_keys = []
|
20
|
+
|
21
|
+
# write the activity to the aggregate set and the owner's feed
|
22
|
+
::Tekeya.redis.multi do
|
23
|
+
write_aggregate(activity_key, attachments)
|
24
|
+
write_to_feed(entity.profile_feed_key, score, activity_key)
|
25
|
+
end
|
26
|
+
|
27
|
+
# trim the profile feed
|
28
|
+
removed_keys += trim_feed(entity.profile_feed_key)
|
29
|
+
|
30
|
+
# Fanout the activity to the owner's trackers
|
31
|
+
entity_trackers_feeds.each do |feed_key|
|
32
|
+
# write the activity to the tracker's feed
|
33
|
+
::Tekeya.redis.multi do
|
34
|
+
write_to_feed(feed_key, score, activity_key)
|
35
|
+
end
|
36
|
+
|
37
|
+
# trim the tracker's feed
|
38
|
+
removed_keys += trim_feed(feed_key)
|
39
|
+
end
|
40
|
+
|
41
|
+
# cleanup the garbage
|
42
|
+
collect_garbage removed_keys
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# Writes the activity and its' attachments to the aggregate set
|
48
|
+
#
|
49
|
+
# @param [String] activity_key the key of the activity to be added
|
50
|
+
# @param [Array] attachments an array of attachments associated with the activity
|
51
|
+
def self.write_aggregate(activity_key, attachments)
|
52
|
+
# save the aggregate set
|
53
|
+
attachments.each do |attachment|
|
54
|
+
::Tekeya.redis.sadd(activity_key, attachment)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Tekeya
|
2
|
+
module Feed
|
3
|
+
module Activity
|
4
|
+
module Resque
|
5
|
+
# A resque worker to copy activities when an entity tracks another
|
6
|
+
class DeleteActivity
|
7
|
+
include Tekeya::Feed::Activity::Resque
|
8
|
+
|
9
|
+
@queue = :activity_queue
|
10
|
+
|
11
|
+
# @private
|
12
|
+
def self.perform(activity_aggregate_key)
|
13
|
+
# get the activity properties from the key
|
14
|
+
key_components = activity_aggregate_key.split(':')
|
15
|
+
entity_type = key_components[2].safe_constantize
|
16
|
+
entity_id = key_components[3]
|
17
|
+
|
18
|
+
# get the entity
|
19
|
+
entity = entity_type.where(entity_type.entity_primary_key.to_sym => entity_id).first
|
20
|
+
# we only need the feed keys of the trackers
|
21
|
+
entity_trackers_feeds = entity.trackers.map(&:feed_key)
|
22
|
+
entity_trackers_feeds << entity.profile_feed_key
|
23
|
+
|
24
|
+
# remove the aggregate key from the trackers' feeds and prepare the activity for garbage collection
|
25
|
+
::Tekeya.redis.multi do
|
26
|
+
entity_trackers_feeds.each do |feed_key|
|
27
|
+
if ::Tekeya.redis.zrank(feed_key, activity_aggregate_key)
|
28
|
+
# remove the activity aggregate key from the feed
|
29
|
+
::Tekeya.redis.zrem(feed_key, activity_aggregate_key)
|
30
|
+
# decrement the activity counter
|
31
|
+
::Tekeya.redis.decr("#{activity_aggregate_key}:counter")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# trim the tracker feed and cleanup
|
37
|
+
collect_garbage [activity_aggregate_key]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Tekeya
|
2
|
+
module Feed
|
3
|
+
module Activity
|
4
|
+
module Resque
|
5
|
+
# A resque worker to copy activities when an entity tracks another
|
6
|
+
class FeedCopy
|
7
|
+
include Tekeya::Feed::Activity::Resque
|
8
|
+
|
9
|
+
@queue = :activity_queue
|
10
|
+
|
11
|
+
# @private
|
12
|
+
def self.perform(tracked_feed_key, tracker_feed_key)
|
13
|
+
# get the keys to the activities so we can increment the counters later
|
14
|
+
activity_keys = ::Tekeya.redis.zrange(tracked_feed_key, 0, -1)
|
15
|
+
|
16
|
+
::Tekeya.redis.multi do
|
17
|
+
# copy the latest activities from the tracked entity to the tracker feed
|
18
|
+
::Tekeya.redis.zunionstore(tracker_feed_key, [tracker_feed_key, tracked_feed_key])
|
19
|
+
|
20
|
+
# increment the activity counter
|
21
|
+
activity_keys.each do |activity_key|
|
22
|
+
activity_counter_key = "#{activity_key}:counter"
|
23
|
+
::Tekeya.redis.incr(activity_counter_key)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# trim the tracker feed and cleanup
|
28
|
+
collect_garbage trim_feed(tracker_feed_key)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Tekeya
|
2
|
+
module Feed
|
3
|
+
module Activity
|
4
|
+
module Resque
|
5
|
+
# A resque worker to copy activities when an entity tracks another
|
6
|
+
class UntrackFeed
|
7
|
+
include Tekeya::Feed::Activity::Resque
|
8
|
+
|
9
|
+
@queue = :activity_queue
|
10
|
+
|
11
|
+
# @private
|
12
|
+
def self.perform(untracked_feed_key, untracker_feed_key)
|
13
|
+
# get the keys to the activities so we can decrement the counters later
|
14
|
+
activity_keys = ::Tekeya.redis.zrange(untracked_feed_key, 0, -1)
|
15
|
+
|
16
|
+
::Tekeya.redis.multi do
|
17
|
+
# delete the latest activities of the untracked entity from the tracker feed
|
18
|
+
::Tekeya.redis.zrem(untracker_feed_key, activity_keys)
|
19
|
+
|
20
|
+
# increment the activity counter
|
21
|
+
activity_keys.each do |activity_key|
|
22
|
+
activity_counter_key = "#{activity_key}:counter"
|
23
|
+
::Tekeya.redis.decr(activity_counter_key)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# trim the tracker feed and cleanup
|
28
|
+
collect_garbage trim_feed(untracker_feed_key)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|