tekeya 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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