simple-feed 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +30 -0
  3. data/.gitignore +16 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +1156 -0
  6. data/.travis.yml +14 -0
  7. data/.yardopts +3 -0
  8. data/Gemfile +4 -0
  9. data/Guardfile +18 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +457 -0
  12. data/Rakefile +16 -0
  13. data/bin/console +14 -0
  14. data/bin/setup +8 -0
  15. data/examples/hash_provider_example.rb +24 -0
  16. data/examples/redis_provider_example.rb +28 -0
  17. data/examples/shared/provider_example.rb +66 -0
  18. data/lib/simple-feed.rb +1 -0
  19. data/lib/simple_feed.rb +1 -0
  20. data/lib/simplefeed.rb +61 -0
  21. data/lib/simplefeed/activity/base.rb +14 -0
  22. data/lib/simplefeed/activity/multi_user.rb +71 -0
  23. data/lib/simplefeed/activity/single_user.rb +70 -0
  24. data/lib/simplefeed/dsl.rb +38 -0
  25. data/lib/simplefeed/dsl/activities.rb +70 -0
  26. data/lib/simplefeed/dsl/formatter.rb +109 -0
  27. data/lib/simplefeed/event.rb +87 -0
  28. data/lib/simplefeed/feed.rb +78 -0
  29. data/lib/simplefeed/providers.rb +45 -0
  30. data/lib/simplefeed/providers/base/provider.rb +84 -0
  31. data/lib/simplefeed/providers/hash.rb +8 -0
  32. data/lib/simplefeed/providers/hash/paginator.rb +31 -0
  33. data/lib/simplefeed/providers/hash/provider.rb +169 -0
  34. data/lib/simplefeed/providers/proxy.rb +38 -0
  35. data/lib/simplefeed/providers/redis.rb +9 -0
  36. data/lib/simplefeed/providers/redis/boot_info.yml +99 -0
  37. data/lib/simplefeed/providers/redis/driver.rb +158 -0
  38. data/lib/simplefeed/providers/redis/provider.rb +255 -0
  39. data/lib/simplefeed/providers/redis/stats.rb +85 -0
  40. data/lib/simplefeed/providers/serialization/key.rb +82 -0
  41. data/lib/simplefeed/response.rb +77 -0
  42. data/lib/simplefeed/version.rb +3 -0
  43. data/man/running-the-example.png +0 -0
  44. data/man/sf-example.png +0 -0
  45. data/simple-feed.gemspec +44 -0
  46. metadata +333 -0
data/.travis.yml ADDED
@@ -0,0 +1,14 @@
1
+ cache: bundler
2
+ rvm:
3
+ - 2.3.1
4
+ - 2.4.0-preview2
5
+ services:
6
+ - redis-server
7
+ before_script:
8
+ script: bundle exec rspec
9
+ after_success:
10
+ - bundle exec codeclimate-test-reporter
11
+ env:
12
+ notifications:
13
+ slack:
14
+ secure: QaaD9WpCYoGFITVd8ku3cjkw1ADJ/rqsPgFGsbRxjPOj1HDzkkUVs20IK0AysxeI6CPY31rfWqbBcE4I4dUE49uIhpOoBXZ8GDYunUONeEmqGwrqZtV/8NqMLNc0Ouu5Jp/KVlnMO93Pcg97BBW81t1koxqyvgSU5oEmJdTmQZIsRNprhczeCj7T6jbb12LEA8uKi2qnp+FAhL5NJczHxqWCd2pWThtbA+hiRmH/mU0480n1eRForN51jDYUrgxb4PamhfUUG1xkzMPzGsvJTMCQDOG4eyHaq71vlZ3z9BbQG6vbcjfEQHVwfrPRz1k0luIlxMXJxszCFnnLVqeRYs85y4FOQSrLsDfsyLCU+1NgLqtseN6jNKOtY624Ok7YvYzsVYez/CPWUG1d2NIRMYQ+BoL2VQ3SDfvr4bYQ02QmqlhM1bKqmBoM+WmiH1FQk6CKOFcmlSkgkFilLi6YSB/EisQ5V5jo7DnzD38VVACo6J6SgVk2D6soONE3zev34Qa6PIOuwTTlXl6JKH0cpiG058lI+Oza+0xi0jg58sZG3jxeGM7m4wg66htgyN1oRQjIukSR+IJ3PZlFKTLvx2rCUcYz+8fRpPCMTSIVLeju5oPqOm0VRUni2dG5JGyNACV8EhI4ZMcfl7+uFTIagCnllRftBx8lY6AxWN3WpDE=
data/.yardopts ADDED
@@ -0,0 +1,3 @@
1
+ --protected
2
+ --no-private
3
+ --embed-mixin ClassMethods
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in activity-feed.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+ require 'guard/rspec'
3
+
4
+ guard :rspec,
5
+ version: 3,
6
+ cmd: 'bundle exec rspec',
7
+ bundler: true,
8
+ all_after_pass: false,
9
+ all_on_start: false,
10
+ keep_failed: false do
11
+
12
+ watch(%r{.*\.gemspec}) { 'spec' }
13
+ watch(%r{^lib/(.+)\.rb$}) { 'spec' }
14
+ watch(%r{^spec/.+_spec\.rb$})
15
+ watch('spec/spec_helper.rb') { 'spec' }
16
+ watch(%r{spec/support/.*}) { 'spec' }
17
+ end
18
+
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Konstantin Gredeskoul
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,457 @@
1
+ # SimpleFeed — Scalable, easy to use activity feed implementation.
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/simple-feed.svg)](https://badge.fury.io/rb/simple-feed)
4
+ [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/kigster/simple-feed/master/LICENSE.txt)
5
+ [![Build Status](https://travis-ci.org/kigster/simple-feed.svg?branch=master)](https://travis-ci.org/kigster/simple-feed)
6
+ [![Code Climate](https://codeclimate.com/repos/58339a5b3d9faa74ac006b36/badges/8b899f6df4fc1ed93759/gpa.svg)](https://codeclimate.com/repos/58339a5b3d9faa74ac006b36/feed)
7
+ [![Test Coverage](https://codeclimate.com/repos/58339a5b3d9faa74ac006b36/badges/8b899f6df4fc1ed93759/coverage.svg)](https://codeclimate.com/repos/58339a5b3d9faa74ac006b36/coverage)
8
+ [![Issue Count](https://codeclimate.com/repos/58339a5b3d9faa74ac006b36/badges/8b899f6df4fc1ed93759/issue_count.svg)](https://codeclimate.com/repos/58339a5b3d9faa74ac006b36/feed)
9
+
10
+ This is a ruby implementation of a fast simple feed commonly used in a typical social network-like applications. The implementation is optimized for **read-time performance** and high concurrency (lots of users). A default Redis-based provider implementation is provided, with the API supporting new providers very easily.
11
+
12
+ <div style="border: 2px solid #222; padding: 10px; background: #f5f5f5; font-family: 'HelveticaNeue-CondensedBold'; font-size: 14pt;">
13
+ <ol>
14
+ <li>Please note that this project is under <em>active development</em>, and is not yet completed.<br/></li>
15
+ <li>We thank <em><a href="http://simbi.com">Simbi, Inc.</a></em> for sponsoring the development of this open source library.</li>
16
+ </div>
17
+
18
+ ## What is an activity feed?
19
+
20
+ > Activity feed is a visual representation of a time-ordered, reverse chronological list of events which can be:
21
+ >
22
+ > * personalized for a given user or a group, or global
23
+ > * filtered by a certain characteristic, such as, eg.
24
+ > * the source of the events — i.e. people you follow
25
+ > * type of event (i.e. posts, likes, and updates)
26
+ > * the target of the event i.e. my own activity as opposed to from those I follow.
27
+ > * aggregated across several actors for a similar event type, eg. "John, Mary, etc.. followed George"
28
+
29
+ Here is an example of a text-based simple feed that is very common today on social networking sites.
30
+
31
+ [![Example](https://raw.githubusercontent.com/kigster/simple-feed/master/man/sf-example.png)](https://raw.githubusercontent.com/kigster/simple-feed/master/man/sf-example.png)
32
+
33
+ The _stories_ in the feed depend entirely on the application using this
34
+ library, therefore to integrate with SimpleFeed requires implementing
35
+ several _glue points_ in your code.
36
+
37
+ ## Challenges
38
+
39
+ Activity feeds tend to be challenging due to the large number of event types that it typically includes, and the requirement for it to scale to massive numbers of concurrent users. Therefore common implementations tend to focus on either:
40
+
41
+ * optimizing the read time performance by pre-computing the feed for each user ahead of time
42
+ * OR optimizing the various ranking algorithms by computing the feed at read time, with complex forms of caching addressing the performance requirements.
43
+
44
+ The first type of feed is much simpler to implement on a large scale (up to a point), and it scales well if the data is stored in a light-weight in-memory storage such as Redis. This is exactly the approach this library takes.
45
+
46
+ ## Overview
47
+
48
+ The feed library aims to address the following goals:
49
+
50
+ * To define a minimalistic API for a typical event-based simple feed,
51
+ without tying it to any concrete provider implementation
52
+ * To make it easy to implement and plug in a new type of provider,
53
+ eg.using Couchbase or MongoDB
54
+ * To provide a scalable default provider implementation using Redis, which can support millions of users via sharding
55
+ * To support multiple simple feeds within the same application, but used for different purposes, eg. simple feed of my followers, versus simple feed of my own actions.
56
+
57
+ ## Usage
58
+
59
+ First you need to configure the Feed with a valid provider
60
+ implementation and a name.
61
+
62
+ ### Configuration
63
+
64
+ Below we configure a feed called `:newsfeed`, which presumably
65
+ will be populated with the events coming from the followers.
66
+
67
+ ```ruby
68
+ require 'simplefeed'
69
+ require 'simplefeed/providers/redis'
70
+
71
+ # Let's define a Redis-based feed, and wrap Redis in a in a ConnectionPool.
72
+ SimpleFeed.define(:newsfeed) do |f|
73
+ f.provider = SimpleFeed.provider(:redis,
74
+ redis: -> { ::Redis.new },
75
+ pool_size: 10)
76
+ f.per_page = 50
77
+ f.batch_size = 10 # default batch size
78
+ f.namespace = 'nf'
79
+ end
80
+ ```
81
+
82
+ After the feed is defined, the gem creates a similarly named method
83
+ under the `SimpleFeed` namespace to access the feed. For example, given
84
+ a name such as `:newsfeed` the following are all valid ways of
85
+ accessing the feed:
86
+
87
+ * `SimpleFeed.newsfeed`
88
+ * `SimpleFeed.get(:newsfeed)`
89
+
90
+ You can also get a full list of currently defined feeds with `SimpleFeed.feed_names` method.
91
+
92
+ ### Reading from and writing to the feed
93
+
94
+ For the impatient here is a quick way to get started with the
95
+ `SimpleFeed`.
96
+
97
+ ```ruby
98
+ activity = SimpleFeed.get(:followers).activity(@current_user.id)
99
+
100
+ # Store directly the value and the optional time stamp
101
+ activity.store(value: 'hello')
102
+ # => true
103
+
104
+ # or equivalent:
105
+ @event = SimpleFeed::Event.new(value: 'hello', at: Time.now)
106
+ activity.store(event: @event)
107
+ # => false # false indicates that the same event is already in the feed.
108
+ ```
109
+
110
+ As we've added events for this user, we can request them back, sorted by
111
+ the time and paginated. If you are using a distributed provider, such as
112
+ `Redis`, the events can be retrieved by any ruby process in your
113
+ application, not just the one that published the event (which is the
114
+ case for the "toy" `Hash::Provider`.
115
+
116
+ ```ruby
117
+ activity.paginate(page: 1)
118
+ # => [ <SimpleFeed::Event#0x2134afa value='hello' at='2016-11-20 23:32:56 -0800'> ]
119
+ ```
120
+
121
+ ### The Two Forms of the API
122
+
123
+ The feed API is offered in a single-user and a batch (multi-user) forms.
124
+
125
+ The only and primary difference is in what the methods return. In the
126
+ single user case, the return of, say, `#total_count` is an `Integer`
127
+ value representing the total count for this user.
128
+
129
+ In the multi-user case, the return is a `SimpleFeed::Response` instance,
130
+ that can be thought of as a `Hash`, that has the user IDs as the keys,
131
+ and return results for each user as a value.
132
+
133
+ Please see further below the details about the [Batch API](#bach-api).
134
+
135
+ <a name="single-user-api"/>
136
+
137
+ ##### Single-User API
138
+
139
+ This API should be used typically for _read_ operations, and is accessed
140
+ via the `SimpleFeed::Feed#for` instance method. Optimized for simplicity
141
+ of data retrieval of a single-user, this method strives for simplicity
142
+ and ease of use.
143
+
144
+ Below is a user session that demonstrates simple return values from the
145
+ Feed operations for a given user:
146
+
147
+ ```ruby
148
+ require 'simplefeed'
149
+
150
+ # Define the feed using an in-memory Hash provider, which uses
151
+ # SortedSet to keep user's events sorted.
152
+ SimpleFeed.define(:notifications) do |f|
153
+ f.provider = SimpleFeed.provider(:hash)
154
+ f.per_page = 50
155
+ f.per_page = 2
156
+ end
157
+
158
+ # Let's get the Activity instance that wraps this user_id
159
+ activity = SimpleFeed.get(:notifications).activity(user_id)
160
+ # => [... complex object removed for brevity ]
161
+
162
+ # let's clear out this feed to ensure it's empty
163
+ activity.wipe
164
+ # => true
165
+
166
+ # Let's verify that the counts for this feed are at zero
167
+ activity.total_count
168
+ #=> 0
169
+
170
+ activity.unread_count
171
+ #=> 0
172
+
173
+ # Store some events
174
+ activity.store(value: 'hello')
175
+ activity.store(value: 'goodbye')
176
+
177
+ # Now we can paginate the events, which by default resets "last_read" timestamp the user
178
+ activity.paginate(page: 1)
179
+ # [
180
+ # [0] #<SimpleFeed::Event#70138821650220 {"value":"goodbye","at":1480475294.0579991}>,
181
+ # [1] #<SimpleFeed::Event#70138821649420 {"value":"hello","at":1480475294.057138}>
182
+ # ]
183
+
184
+ # Now the unread_count should return 0 since the user just "viewed" the feed.
185
+ activity.unread_count
186
+ #=> 0
187
+ ```
188
+
189
+ You can fetch all items in the feed using `#fetch`, and you can
190
+ `#paginate` without resetting the `last_read` timestamp by passing the
191
+ `peek: true` as a parameter.
192
+
193
+ <a name="batch-api"/>
194
+
195
+ ##### Batch (Multi-User) API
196
+
197
+ This API should be used when dealing with an array of users (or, in the
198
+ future, a Proc or an ActiveRecord relation).
199
+
200
+ > There are several reasons why this API should be preferred for
201
+ > operations that perform a similar action across a range of users:
202
+ > _various provider implementations can be heavily optimized for
203
+ > concurrency, and performance_.
204
+ >
205
+ > The Redis Provider, for example, uses a notion of `pipelining` to send
206
+ > updates for different users asynchronously and concurrently.
207
+
208
+ Multi-user operations return a `SimpleFeed::Response` object, which can
209
+ be used as a hash (keyed on user_id) to fetch the result of a given
210
+ user.
211
+
212
+ ```ruby
213
+ # Using the Feed API with, eg #find_in_batches
214
+ @event_producer.followers.find_in_batches do |group|
215
+
216
+ # Convert a group to the array of IDs and get ready to store
217
+ activity = SimpleFeed.get(:followers).activity(group.map(&:id))
218
+ activity.store(value: "#{@event_producer.name} liked an article")
219
+
220
+ # => [Response] { user_id1 => [Boolean], user_id2 => [Boolean]... }
221
+ # true if the value was stored, false if it wasn't.
222
+ end
223
+ ```
224
+
225
+ ##### DSL
226
+
227
+ The library offers a convenient DSL for adding feed functionality into
228
+ your current scope.
229
+
230
+ To use the module, just include `SimpleFeed::DSL` where needed, which
231
+ exports just one primary method `with_activity'. You call this method
232
+ and pass an activity object created for a set of users (or a single
233
+ user), like so:
234
+
235
+ ```ruby
236
+ require 'simplefeed/dsl'
237
+ include SimpleFeed::DSL
238
+
239
+ feed = SimpleFeed.newsfeed
240
+ activity = feed.activity(current_user.id)
241
+ data_to_store = %w(France Germany England)
242
+
243
+ def report(value)
244
+ puts value
245
+ end
246
+
247
+ with_activity(activity, countries: data_to_store) do
248
+ # we can use countries as a variable because it was passed above in **opts
249
+ countries.each do |country|
250
+ # we can call #store without a receiver because the block is passed to
251
+ # instance_eval
252
+ store(value: country) { |result| report(result ? 'success' : 'failure') }
253
+ # we can call #report inside the proc because it is evaluated in the
254
+ # outside context of the #with_activity
255
+ end
256
+ printf "Activity counts are: %d unread of %d total\n", unread_count, total_count
257
+ end
258
+ ```
259
+
260
+ <a name="api"/>
261
+
262
+ ## Complete API
263
+
264
+ ### Single User
265
+
266
+ For a single user, via the instance of
267
+ `SimpleFeed::Activity::UserActivity` class:
268
+
269
+ ```ruby
270
+ require 'simplefeed'
271
+
272
+ @ua = SimpleFeed.get(:news).activity(current_user.id)
273
+
274
+ @ua.store(event:)
275
+ @ua.store(value:, at:)
276
+ # => [Boolean] true if the value was stored, false if it wasn't.
277
+
278
+ @ua.delete(event:)
279
+ @ua.delete(value:, at:)
280
+ # => [Boolean] true if the value was removed, false if it didn't exist
281
+
282
+ @ua.delete_if do |user_id, event|
283
+ # if the block returns true, the event is deleted
284
+ end
285
+
286
+ @ua.wipe
287
+ # => [Boolean] true
288
+
289
+ # Options:
290
+ # with options[:peak] = true it does not reset last_read
291
+ # with options[:with_total] = true it returns a hash with a total:
292
+ # @return:
293
+ @ua.paginate(page:, per_page:, **options)
294
+ # @return: [Array]<Event> (without options[:with_total])
295
+ # @return: { events: [Array]<Event, total_count: 3242 }
296
+
297
+ @ua.fetch
298
+ # => [Array]<Event> – returns all events up to Feed.max_size
299
+
300
+ @ua.reset_last_read
301
+ # => [Time] last_read
302
+
303
+ @ua.total_count
304
+ # => [Integer] total_count
305
+
306
+ @ua.unread_count
307
+ # => [Integer] unread_count
308
+
309
+ @ua.last_read
310
+ # => [Time] last_read
311
+ ```
312
+
313
+ #### Batch User API
314
+
315
+ Each API call at this level expects an array of user IDs, therefore the
316
+ return value is an object, `SimpleFeed::Response`, containing individual
317
+ responses for each user, accessible via `response[user_id]` method.
318
+
319
+ ```ruby
320
+ @multi = SimpleFeed.get(:feed_name).activity(User.active.map(&:id))
321
+
322
+ @multi.store(value:, at:)
323
+ @multi.store(event:)
324
+ # => [Response] { user_id => [Boolean], ... } true if the value was stored, false if it wasn't.
325
+
326
+ @multi.delete(value:, at:)
327
+ @multi.delete(event:)
328
+ # => [Response] { user_id => [Boolean], ... } true if the value was removed, false if it didn't exist
329
+
330
+ @multi.delete_if do |user_id, event|
331
+ # if the block returns true, the event is deleted
332
+ end
333
+
334
+ @multi.wipe
335
+ # => [Response] { user_id => [Boolean], ... } true if user activity was found and deleted, false otherwise
336
+
337
+ @multi.paginate(page:, per_page:, peek: false)
338
+ # => [Response] { user_id => [Array]<Event>, ... }
339
+ # With (peak: true) does not reset last_read, otherwise it does.
340
+
341
+ @multi.fetch
342
+ # => [Response] { user_id => [Array]<Event>, ... }
343
+
344
+ @multi.reset_last_read
345
+ # => [Response] { user_id => [Time] last_read, ... }
346
+
347
+ @multi.total_count
348
+ # => [Response] { user_id => [Integer] total_count, ... }
349
+
350
+ @multi.unread_count
351
+ # => [Response] { user_id => [Integer] unread_count, ... }
352
+
353
+ @multi.last_read
354
+ # => [Response] { user_id => [Time] last_read, ... }
355
+
356
+ ```
357
+
358
+ ## Providers
359
+
360
+ A provider is an underlying implementation that persists the events for each user, together with some meta-data for each feed.
361
+
362
+ It is the intention of this gem that:
363
+
364
+ * it should be easy to swap providers
365
+ * it should be easy to add new providers
366
+
367
+ Each provider must implement exactly the public API of a provider shown
368
+ above (the `Feed` version, that receives `user_ids:` as arguments).
369
+
370
+ Two providers are available with this gem:
371
+
372
+ * `SimpleFeed::Providers::Redis::Provider` is the production-ready provider that uses the [sorted set Redis data type](https://redislabs.com/ebook/redis-in-action/part-2-core-concepts-2/chapter-3-commands-in-redis/3-5-sorted-sets) and their operations operations to store the events, scored by their time typically (but not necessarily). This provider is highly optimized for massive writes and can be sharded by using a _Twemproxy_ backend, and many small Redis shards.
373
+
374
+ * `SimpleFeed::Providers::HashProvider` is a pure Hash-like implementation of a provider that can be useful in unit tests of a host application. This provider could be used to write and read events within a single ruby process, can be serialized to and from a YAML file, and is therefore intended primarily for Feed emulations in automated tests.
375
+
376
+
377
+ ### Redis Provider
378
+
379
+ If you set environment variable `REDIS_DEBUG` and run the example (see below) you will see every operation redis performs. This could be useful in debugging an issue or submitting a bug report.
380
+
381
+ ## Examples
382
+
383
+ Source code for the gem contains the `examples` folder with an example file that can be used to measure the performance of the Redis-based provider.
384
+
385
+ To run it, checkout the source of the library, and then:
386
+
387
+ ```bash
388
+ git clone https://github.com/kigster/simple-feed.git
389
+ cd simple-feed
390
+ bundle
391
+ be rspec # make sure tests are passing
392
+ ruby examples/redis_provider_example.rb
393
+ ```
394
+
395
+ The above command will help you download, setup all dependencies, and run the examples for a single user. To run examples for multiple users, just __just pass a number as a second argument__:, for example:
396
+
397
+ ``` bash
398
+ ruby examples/redis_provider_example.rb 10
399
+ ```
400
+
401
+ Or to measure the time:
402
+ ```bash
403
+ time ruby examples/redis_provider_example.rb 1000 > /dev/null
404
+ ```
405
+
406
+ Below is a an example output shown for a single user:
407
+
408
+ [![Example](https://raw.githubusercontent.com/kigster/simple-feed/master/man/running-the-example.png)](https://raw.githubusercontent.com/kigster/simple-feed/master/man/running-the-example.png)
409
+
410
+ ### Generating Ruby API Documentation
411
+
412
+ ```bash
413
+ rake doc
414
+ ```
415
+
416
+ This should use Yard to generate the documentation, and open your browser once it's finished.
417
+
418
+ ### Installation
419
+
420
+ Add this line to your application's Gemfile:
421
+
422
+ ```ruby
423
+ gem 'simple-feed'
424
+ ```
425
+
426
+ And then execute:
427
+
428
+ ```
429
+ $ bundle
430
+ ```
431
+
432
+ Or install it yourself as:
433
+
434
+ ```
435
+ $ gem install simple-feed
436
+ ```
437
+
438
+ ### Development
439
+
440
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
441
+
442
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
443
+
444
+ ### Contributing
445
+
446
+ Bug reports and pull requests are welcome on GitHub at https://github.com/kigster/simple-feed
447
+
448
+ ### License
449
+
450
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
451
+
452
+ ### Acknowledgements
453
+
454
+ * This project is conceived and sponsored by [Simbi, Inc.](https://simbi.com).
455
+ * Author's personal experience at [Wanelo, Inc.](https://wanelo.com) has served as an inspiration.
456
+
457
+