tekeya 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. data/.document +0 -0
  2. data/.gitignore +23 -0
  3. data/.rspec +3 -0
  4. data/.yardopts +1 -0
  5. data/Gemfile +39 -0
  6. data/Guardfile +40 -0
  7. data/LICENSE +22 -0
  8. data/README.md +37 -0
  9. data/Rakefile +13 -0
  10. data/TODO.todo +8 -0
  11. data/app/active_record/tekeya/activity.rb +7 -0
  12. data/app/active_record/tekeya/attachment.rb +5 -0
  13. data/app/active_record/tekeya/notification.rb +5 -0
  14. data/app/mongoid/tekeya/activity.rb +9 -0
  15. data/app/mongoid/tekeya/attachment.rb +6 -0
  16. data/app/mongoid/tekeya/notification.rb +10 -0
  17. data/db/migrate/00_create_activities.rb +11 -0
  18. data/db/migrate/01_create_attachments.rb +13 -0
  19. data/db/migrate/02_create_notifications.rb +14 -0
  20. data/lib/tasks/resque_tasks.rake +3 -0
  21. data/lib/tekeya.rb +81 -0
  22. data/lib/tekeya/configuration.rb +48 -0
  23. data/lib/tekeya/entity.rb +432 -0
  24. data/lib/tekeya/entity/group.rb +22 -0
  25. data/lib/tekeya/errors/tekeya_error.rb +11 -0
  26. data/lib/tekeya/errors/tekeya_fatal.rb +11 -0
  27. data/lib/tekeya/errors/tekeya_non_entity.rb +6 -0
  28. data/lib/tekeya/errors/tekeya_non_group.rb +6 -0
  29. data/lib/tekeya/errors/tekeya_relation_already_exists.rb +6 -0
  30. data/lib/tekeya/errors/tekeya_relation_non_existent.rb +6 -0
  31. data/lib/tekeya/feed/activity.rb +101 -0
  32. data/lib/tekeya/feed/activity/feed_item.rb +58 -0
  33. data/lib/tekeya/feed/activity/resque.rb +56 -0
  34. data/lib/tekeya/feed/activity/resque/activity_fanout.rb +61 -0
  35. data/lib/tekeya/feed/activity/resque/delete_activity.rb +43 -0
  36. data/lib/tekeya/feed/activity/resque/feed_copy.rb +34 -0
  37. data/lib/tekeya/feed/activity/resque/untrack_feed.rb +34 -0
  38. data/lib/tekeya/feed/attachable.rb +15 -0
  39. data/lib/tekeya/feed/attachment.rb +19 -0
  40. data/lib/tekeya/feed/notification.rb +93 -0
  41. data/lib/tekeya/railtie.rb +18 -0
  42. data/lib/tekeya/version.rb +3 -0
  43. data/spec/fabricators/attachment_fabricator.rb +3 -0
  44. data/spec/fabricators/group_fabricator.rb +4 -0
  45. data/spec/fabricators/status_fabricator.rb +3 -0
  46. data/spec/fabricators/user_fabricator.rb +3 -0
  47. data/spec/orm/active_record.rb +14 -0
  48. data/spec/orm/mongoid.rb +6 -0
  49. data/spec/rails_app/Rakefile +7 -0
  50. data/spec/rails_app/app/active_record/group.rb +3 -0
  51. data/spec/rails_app/app/active_record/status.rb +3 -0
  52. data/spec/rails_app/app/active_record/user.rb +3 -0
  53. data/spec/rails_app/app/controllers/application_controller.rb +3 -0
  54. data/spec/rails_app/app/helpers/application_helper.rb +2 -0
  55. data/spec/rails_app/app/mongoid/group.rb +6 -0
  56. data/spec/rails_app/app/mongoid/status.rb +6 -0
  57. data/spec/rails_app/app/mongoid/user.rb +7 -0
  58. data/spec/rails_app/app/views/layouts/application.html.erb +14 -0
  59. data/spec/rails_app/config.ru +4 -0
  60. data/spec/rails_app/config/application.rb +35 -0
  61. data/spec/rails_app/config/boot.rb +8 -0
  62. data/spec/rails_app/config/database.yml +21 -0
  63. data/spec/rails_app/config/environment.rb +5 -0
  64. data/spec/rails_app/config/environments/development.rb +18 -0
  65. data/spec/rails_app/config/environments/production.rb +33 -0
  66. data/spec/rails_app/config/environments/test.rb +33 -0
  67. data/spec/rails_app/config/initializers/backtrace_silencers.rb +7 -0
  68. data/spec/rails_app/config/initializers/configure_mongoid.rb +6 -0
  69. data/spec/rails_app/config/initializers/inflections.rb +15 -0
  70. data/spec/rails_app/config/initializers/resque.rb +1 -0
  71. data/spec/rails_app/config/initializers/secret_token.rb +7 -0
  72. data/spec/rails_app/config/locales/en.yml +5 -0
  73. data/spec/rails_app/config/routes.rb +58 -0
  74. data/spec/rails_app/db/migrate/100_create_users.rb +9 -0
  75. data/spec/rails_app/db/migrate/101_create_groups.rb +11 -0
  76. data/spec/rails_app/db/migrate/102_create_statuses.rb +9 -0
  77. data/spec/rails_app/db/seeds.rb +7 -0
  78. data/spec/rails_app/public/404.html +26 -0
  79. data/spec/rails_app/public/422.html +26 -0
  80. data/spec/rails_app/public/500.html +25 -0
  81. data/spec/rails_app/public/favicon.ico +0 -0
  82. data/spec/rails_app/public/index.html +241 -0
  83. data/spec/rails_app/public/robots.txt +5 -0
  84. data/spec/rails_app/script/rails +6 -0
  85. data/spec/spec_helper.rb +78 -0
  86. data/spec/tekeya/activity_spec.rb +170 -0
  87. data/spec/tekeya/entity_spec.rb +158 -0
  88. data/spec/tekeya/notification_spec.rb +49 -0
  89. data/spec/tekeya_helper.rb +7 -0
  90. data/spec/tekeya_spec.rb +51 -0
  91. data/tekeya.gemspec +22 -0
  92. 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,11 @@
1
+ module Tekeya
2
+ module Errors
3
+ class TekeyaError < ::StandardError
4
+ def initialize(message)
5
+ super(message)
6
+ ::ActiveSupport::Notifications.
7
+ instrument('tekeya_error.tekeya', :message => message)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Tekeya
2
+ module Errors
3
+ class TekeyaFatal < ::StandardError
4
+ def initialize(message)
5
+ super(message)
6
+ ::ActiveSupport::Notifications.
7
+ instrument('tekeya_fatal.tekeya', :message => message)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,6 @@
1
+ module Tekeya
2
+ module Errors
3
+ class TekeyaNonEntity < TekeyaError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Tekeya
2
+ module Errors
3
+ class TekeyaNonGroup < TekeyaError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Tekeya
2
+ module Errors
3
+ class TekeyaRelationAlreadyExists < TekeyaError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Tekeya
2
+ module Errors
3
+ class TekeyaRelationNonExistent < TekeyaError
4
+ end
5
+ end
6
+ 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