├── .bnsignore ├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── History.txt ├── MIT-LICENSE ├── README.md ├── Rakefile ├── lib ├── redis-timeline.rb ├── tasks │ └── timeline_tasks.rake └── timeline │ ├── activity.rb │ ├── actor.rb │ ├── config.rb │ ├── helpers.rb │ ├── track.rb │ └── version.rb ├── redis_timeline.gemspec └── spec ├── activity_spec.rb ├── actor_spec.rb ├── redis-test.conf ├── spec_helper.rb ├── stdout ├── timeline_spec.rb └── track_spec.rb /.bnsignore: -------------------------------------------------------------------------------- 1 | # The list of files that should be ignored by Mr Bones. 2 | # Lines that start with '#' are comments. 3 | # 4 | # A .gitignore file can be used instead by setting it as the ignore 5 | # file in your Rakefile: 6 | # 7 | # Bones { 8 | # ignore_file '.gitignore' 9 | # } 10 | # 11 | # For a project with a C extension, the following would be a good set of 12 | # exclude patterns (uncomment them if you want to use them): 13 | # *.[oa] 14 | # *~ 15 | announcement.txt 16 | coverage 17 | doc 18 | pkg 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | log/*.log 3 | pkg/ 4 | test/dummy/db/*.sqlite3 5 | test/dummy/log/*.log 6 | test/dummy/tmp/ 7 | test/dummy/.sass-cache 8 | spec/*.rdb 9 | *.gem -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.0.0 4 | - 1.9.3 5 | 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Declare your gem's dependencies in timeline.gemspec. 4 | # Bundler will treat runtime dependencies like base dependencies, and 5 | # development dependencies will be added by default to the :development group. 6 | gemspec 7 | 8 | # Declare any dependencies that are still in development here instead of in 9 | # your gemspec. These might include edge Rails or gems from your path or 10 | # Git. Remember to move these dependencies to your gemspec before releasing 11 | # your gem to rubygems.org. 12 | 13 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | redis-timeline (0.2.1) 5 | activemodel 6 | activesupport 7 | hashie 8 | multi_json 9 | rake 10 | redis 11 | redis-namespace 12 | 13 | GEM 14 | remote: http://rubygems.org/ 15 | specs: 16 | activemodel (4.1.6) 17 | activesupport (= 4.1.6) 18 | builder (~> 3.1) 19 | activesupport (4.1.6) 20 | i18n (~> 0.6, >= 0.6.9) 21 | json (~> 1.7, >= 1.7.7) 22 | minitest (~> 5.1) 23 | thread_safe (~> 0.1) 24 | tzinfo (~> 1.1) 25 | builder (3.2.2) 26 | diff-lcs (1.2.5) 27 | hashie (3.3.1) 28 | i18n (0.6.11) 29 | json (1.8.1) 30 | minitest (5.4.1) 31 | multi_json (1.10.1) 32 | rake (10.3.2) 33 | redis (3.1.0) 34 | redis-namespace (1.5.1) 35 | redis (~> 3.0, >= 3.0.4) 36 | rspec (3.1.0) 37 | rspec-core (~> 3.1.0) 38 | rspec-expectations (~> 3.1.0) 39 | rspec-mocks (~> 3.1.0) 40 | rspec-core (3.1.4) 41 | rspec-support (~> 3.1.0) 42 | rspec-expectations (3.1.1) 43 | diff-lcs (>= 1.2.0, < 2.0) 44 | rspec-support (~> 3.1.0) 45 | rspec-mocks (3.1.1) 46 | rspec-support (~> 3.1.0) 47 | rspec-support (3.1.0) 48 | sqlite3 (1.3.9) 49 | thread_safe (0.3.4) 50 | tzinfo (1.2.2) 51 | thread_safe (~> 0.1) 52 | 53 | PLATFORMS 54 | ruby 55 | 56 | DEPENDENCIES 57 | redis-timeline! 58 | rspec 59 | sqlite3 60 | -------------------------------------------------------------------------------- /History.txt: -------------------------------------------------------------------------------- 1 | == 0.1.4 / 2012-02-28 2 | 3 | * fix bug in extra fields 4 | * add activity to user's posts timeline 5 | 6 | == 0.1.3 / 2012-02-28 7 | 8 | * Accept :extra_fields as an option for track, to cache extra fields on the timeline post. 9 | 10 | == 0.1.2 / 2012-02-25 11 | 12 | * Add ActiveModel::Naming to Timeline::Activity so that it can be passed to the render method in Rails. 13 | 14 | 15 | == 0.1.1 / 2012-02-24 16 | 17 | * Add json-ified object to lists. 18 | 19 | 20 | == 0.1.0 / 2012-02-17 21 | 22 | * First commit 23 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2012 Felix Clack 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | redis-timeline 2 | =========== 3 | 4 | [![Build Status](https://travis-ci.org/felixclack/redis-timeline.png?branch=master)](https://travis-ci.org/felixclack/redis-timeline) 5 | 6 | Redis backed timelines in your app. 7 | 8 | 9 | Pair program with me! 11 | 12 | 13 | Features 14 | -------- 15 | 16 | * store your timeline in Redis. 17 | 18 | Examples 19 | -------- 20 | 21 | The simple way... 22 | 23 | class Post < ActiveRecord::Base 24 | include Timeline::Track 25 | 26 | track :new_post 27 | end 28 | 29 | By default, track fires in the `after_create` callback of your model and uses `self` as the object and `creator` as the actor. 30 | 31 | You can specify these options explicity... 32 | 33 | class Comment < ActiveRecord::Base 34 | include Timeline::Track 35 | belongs_to :author, class_name: "User" 36 | belongs_to :post 37 | 38 | track :new_comment, 39 | actor: :author, 40 | followers: :post_participants, 41 | object: [:body], 42 | on: :update, 43 | target: :post 44 | 45 | delegate :participants, to: :post, prefix: true 46 | end 47 | 48 | Parameters 49 | ---------- 50 | 51 | `track` accepts the following parameters... 52 | 53 | the first param is the verb name. 54 | 55 | The rest all fit neatly in an options hash. 56 | 57 | * `on:` [ActiveModel callback] 58 | You use it to specify whether you want the timeline activity created after a create, update or destroy. 59 | Default: :create 60 | 61 | * `actor:` [the method that specifies the object that took this action] 62 | In the above example, comment.author is this object. 63 | Default: :creator, so make sure this exists if you don't specify a method here 64 | 65 | * `object:` defaults to self, which is good most of the time. 66 | You can override it if you need to 67 | 68 | * `target:` [related to the `:object` method above. In the example this is the post related to the comment] 69 | default: nil 70 | 71 | * `followers:` [who should see this story in their timeline. This references a method on the actor] 72 | Defaults to the method `followers` defined by Timeline::Actor. 73 | 74 | * `extra_fields:` [accepts an array of method names that you would like to cache the value of in your timeline] 75 | Defaults to nil. 76 | 77 | * `if:` symbol or proc/lambda lets you put conditions on when to track. 78 | 79 | Display a timeline 80 | ------------------ 81 | 82 | To retrieve a timeline for a user... 83 | 84 | class User < ActiveRecord::Base 85 | include Timeline::Actor 86 | end 87 | 88 | The timeline objects are just hashes that are extended by [Hashie](http://github.com/intridea/hashie) to provide method access to the keys. 89 | 90 | user = User.find(1) 91 | user.timeline # => [] 92 | 93 | Requirements 94 | ------------ 95 | 96 | * redis 97 | * active_support 98 | * hashie 99 | 100 | Install 101 | ------- 102 | 103 | Install redis. 104 | 105 | Add to your Gemfile: 106 | 107 | gem 'redis-timeline' 108 | 109 | Or install it by hand: 110 | 111 | gem install redis-timeline 112 | 113 | Setup your redis instance. For a Rails app, something like this... 114 | 115 | # in config/initializers/redis.rb 116 | 117 | Timeline.redis = "localhost:6379/timeline" 118 | 119 | Author 120 | ------ 121 | 122 | Original author: Felix Clack 123 | 124 | License 125 | ------- 126 | 127 | (The MIT License) 128 | 129 | Copyright (c) 2012 Felix Clack 130 | 131 | Permission is hereby granted, free of charge, to any person obtaining 132 | a copy of this software and associated documentation files (the 133 | 'Software'), to deal in the Software without restriction, including 134 | without limitation the rights to use, copy, modify, merge, publish, 135 | distribute, sublicense, and/or sell copies of the Software, and to 136 | permit persons to whom the Software is furnished to do so, subject to 137 | the following conditions: 138 | 139 | The above copyright notice and this permission notice shall be 140 | included in all copies or substantial portions of the Software. 141 | 142 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 143 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 144 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 145 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 146 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 147 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 148 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 149 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | begin 3 | require 'bundler/setup' 4 | rescue LoadError 5 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 6 | end 7 | begin 8 | require 'rdoc/task' 9 | rescue LoadError 10 | require 'rdoc/rdoc' 11 | require 'rake/rdoctask' 12 | RDoc::Task = Rake::RDocTask 13 | end 14 | 15 | RDoc::Task.new(:rdoc) do |rdoc| 16 | rdoc.rdoc_dir = 'rdoc' 17 | rdoc.title = 'Timeline' 18 | rdoc.options << '--line-numbers' 19 | rdoc.rdoc_files.include('README.rdoc') 20 | rdoc.rdoc_files.include('lib/**/*.rb') 21 | end 22 | 23 | 24 | 25 | 26 | Bundler::GemHelper.install_tasks 27 | 28 | require 'rake/testtask' 29 | 30 | Rake::TestTask.new(:test) do |t| 31 | t.libs << 'lib' 32 | t.libs << 'test' 33 | t.pattern = 'test/**/*_test.rb' 34 | t.verbose = false 35 | end 36 | 37 | 38 | task :default => :test 39 | -------------------------------------------------------------------------------- /lib/redis-timeline.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/concern' 2 | require 'multi_json' 3 | require 'hashie' 4 | require 'timeline/config' 5 | require 'timeline/helpers' 6 | require 'timeline/track' 7 | require 'timeline/actor' 8 | require 'timeline/activity' 9 | 10 | module Timeline 11 | extend Config 12 | extend Helpers 13 | end 14 | 15 | -------------------------------------------------------------------------------- /lib/tasks/timeline_tasks.rake: -------------------------------------------------------------------------------- 1 | # desc "Explaining what the task does" 2 | # task :timeline do 3 | # # Task goes here 4 | # end 5 | -------------------------------------------------------------------------------- /lib/timeline/activity.rb: -------------------------------------------------------------------------------- 1 | require 'active_model' 2 | 3 | module Timeline 4 | class Activity < Hashie::Mash 5 | extend ActiveModel::Naming 6 | 7 | def to_partial_path 8 | "timelines/#{verb}" 9 | end 10 | end 11 | end -------------------------------------------------------------------------------- /lib/timeline/actor.rb: -------------------------------------------------------------------------------- 1 | module Timeline 2 | module Actor 3 | extend ActiveSupport::Concern 4 | 5 | included do 6 | def timeline(options={}) 7 | ::Timeline.get_list(timeline_options(options)).map do |item| 8 | ::Timeline::Activity.new ::Timeline.decode(item) 9 | end 10 | end 11 | 12 | def followers 13 | [] 14 | end 15 | 16 | private 17 | def timeline_options(options) 18 | defaults = { list_name: "user:id:#{self.id}:activity", start: 0, end: 19 } 19 | if options.is_a? Hash 20 | defaults.merge!(options) 21 | elsif options.is_a? Symbol 22 | case options 23 | when :global 24 | defaults.merge!(list_name: "global:activity") 25 | when :posts 26 | defaults.merge!(list_name: "user:id:#{self.id}:posts") 27 | when :mentions 28 | defaults.merge!(list_name: "user:id:#{self.id}:mentions") 29 | end 30 | end 31 | end 32 | end 33 | end 34 | end -------------------------------------------------------------------------------- /lib/timeline/config.rb: -------------------------------------------------------------------------------- 1 | require 'redis/namespace' 2 | 3 | module Timeline 4 | module Config 5 | 6 | # Accepts: 7 | # 1. A 'hostname:port' String 8 | # 2. A 'hostname:port:db' String (to select the Redis db) 9 | # 3. A 'hostname:port/namespace' String (to set the Redis namespace) 10 | # 4. A Redis URL String 'redis://host:port' 11 | # 5. An instance of `Redis`, `Redis::Client`, `Redis::DistRedis`, 12 | # or `Redis::Namespace`. 13 | def redis=(server) 14 | case server 15 | when String 16 | if server =~ /redis\:\/\// 17 | redis = Redis.connect(:url => server, :thread_safe => true) 18 | else 19 | server, namespace = server.split('/', 2) 20 | host, port, db = server.split(':') 21 | redis = Redis.new(:host => host, :port => port, 22 | :thread_safe => true, :db => db) 23 | end 24 | namespace ||= :timeline 25 | 26 | @redis = Redis::Namespace.new(namespace, :redis => redis) 27 | when Redis::Namespace 28 | @redis = server 29 | else 30 | @redis = Redis::Namespace.new(:timeline, :redis => server) 31 | end 32 | end 33 | 34 | # Returns the current Redis connection. If none has been created, will 35 | # create a new one. 36 | def redis 37 | return @redis if @redis 38 | self.redis = Redis.respond_to?(:connect) ? Redis.connect : "localhost:6379" 39 | self.redis 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/timeline/helpers.rb: -------------------------------------------------------------------------------- 1 | module Timeline 2 | module Helpers 3 | class DecodeException < StandardError; end 4 | 5 | def encode(object) 6 | ::MultiJson.encode(object) 7 | end 8 | 9 | def decode(object) 10 | return unless object 11 | 12 | begin 13 | ::MultiJson.decode(object) 14 | rescue ::MultiJson::DecodeError => e 15 | raise DecodeException, e 16 | end 17 | end 18 | 19 | def get_list(options={}) 20 | Timeline.redis.lrange options[:list_name], options[:start], options[:end] 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/timeline/track.rb: -------------------------------------------------------------------------------- 1 | module Timeline::Track 2 | extend ActiveSupport::Concern 3 | 4 | module ClassMethods 5 | def track(name, options={}) 6 | @name = name 7 | @callback = options.delete :on 8 | @callback ||= :create 9 | @actor = options.delete :actor 10 | @actor ||= :creator 11 | @object = options.delete :object 12 | @target = options.delete :target 13 | @followers = options.delete :followers 14 | @followers ||= :followers 15 | @mentionable = options.delete :mentionable 16 | 17 | method_name = "track_#{@name}_after_#{@callback}".to_sym 18 | define_activity_method method_name, actor: @actor, object: @object, target: @target, followers: @followers, verb: name, mentionable: @mentionable 19 | 20 | send "after_#{@callback}".to_sym, method_name, if: options.delete(:if) 21 | end 22 | 23 | private 24 | def define_activity_method(method_name, options={}) 25 | define_method method_name do 26 | @actor = send(options[:actor]) 27 | @fields_for = {} 28 | @object = set_object(options[:object]) 29 | @target = !options[:target].nil? ? send(options[:target].to_sym) : nil 30 | @extra_fields ||= nil 31 | @followers = @actor.send(options[:followers].to_sym) 32 | @mentionable = options[:mentionable] 33 | add_activity activity(verb: options[:verb]) 34 | end 35 | end 36 | end 37 | 38 | protected 39 | def activity(options={}) 40 | { 41 | verb: options[:verb], 42 | actor: options_for(@actor), 43 | object: options_for(@object), 44 | target: options_for(@target), 45 | created_at: Time.now 46 | } 47 | end 48 | 49 | def add_activity(activity_item) 50 | redis_add "global:activity", activity_item 51 | add_activity_to_user(activity_item[:actor][:id], activity_item) 52 | add_activity_by_user(activity_item[:actor][:id], activity_item) 53 | add_mentions(activity_item) 54 | add_activity_to_followers(activity_item) if @followers.any? 55 | end 56 | 57 | def add_activity_by_user(user_id, activity_item) 58 | redis_add "user:id:#{user_id}:posts", activity_item 59 | end 60 | 61 | def add_activity_to_user(user_id, activity_item) 62 | redis_add "user:id:#{user_id}:activity", activity_item 63 | end 64 | 65 | def add_activity_to_followers(activity_item) 66 | @followers.each { |follower| add_activity_to_user(follower.id, activity_item) } 67 | end 68 | 69 | def add_mentions(activity_item) 70 | return unless @mentionable and @object.send(@mentionable) 71 | @object.send(@mentionable).scan(/@\w+/).each do |mention| 72 | if user = @actor.class.find_by_username(mention[1..-1]) 73 | add_mention_to_user(user.id, activity_item) 74 | end 75 | end 76 | end 77 | 78 | def add_mention_to_user(user_id, activity_item) 79 | redis_add "user:id:#{user_id}:mentions", activity_item 80 | end 81 | 82 | def extra_fields_for(object) 83 | return {} unless @fields_for.has_key?(object.class.to_s.downcase.to_sym) 84 | @fields_for[object.class.to_s.downcase.to_sym].inject({}) do |sum, method| 85 | sum[method.to_sym] = @object.send(method.to_sym) 86 | sum 87 | end 88 | end 89 | 90 | def options_for(target) 91 | if !target.nil? 92 | { 93 | id: target.id, 94 | class: target.class.to_s, 95 | display_name: target.to_s 96 | }.merge(extra_fields_for(target)) 97 | else 98 | nil 99 | end 100 | end 101 | 102 | def redis_add(list, activity_item) 103 | Timeline.redis.lpush list, Timeline.encode(activity_item) 104 | end 105 | 106 | def set_object(object) 107 | case 108 | when object.is_a?(Symbol) 109 | send(object) 110 | when object.is_a?(Array) 111 | @fields_for[self.class.to_s.downcase.to_sym] = object 112 | self 113 | else 114 | self 115 | end 116 | end 117 | 118 | end -------------------------------------------------------------------------------- /lib/timeline/version.rb: -------------------------------------------------------------------------------- 1 | module Timeline 2 | VERSION = "0.2.1" 3 | end 4 | -------------------------------------------------------------------------------- /redis_timeline.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | 3 | # Maintain your gem's version: 4 | require "timeline/version" 5 | 6 | # Describe your gem and declare its dependencies: 7 | Gem::Specification.new do |s| 8 | s.name = "redis-timeline" 9 | s.version = Timeline::VERSION 10 | s.authors = ["Felix Clack"] 11 | s.email = ["felixclack@gmail.com"] 12 | s.homepage = "http://felixclack.github.com/redis-timeline" 13 | s.summary = "Redis backed timeline for your activity feeds." 14 | s.description = "" 15 | 16 | s.files = Dir["{app,config,db,lib}/**/*"] + ["MIT-LICENSE", "Rakefile", "README.md"] 17 | s.test_files = Dir["spec/**/*"] 18 | 19 | s.add_dependency "activesupport" 20 | s.add_dependency "activemodel" 21 | s.add_dependency "multi_json" 22 | s.add_dependency "rake" 23 | s.add_dependency "redis" 24 | s.add_dependency "hashie" 25 | s.add_dependency "redis-namespace" 26 | 27 | s.add_development_dependency "sqlite3" 28 | s.add_development_dependency "rspec" 29 | end 30 | -------------------------------------------------------------------------------- /spec/activity_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), %w[spec_helper]) 2 | 3 | describe Timeline::Activity do 4 | describe "initialized with json" do 5 | let(:json) { { id: "1", verb: "new_post" } } 6 | 7 | it "returns a Hashie-fied object" do 8 | expect(Timeline::Activity.new(json).id).to eq("1") 9 | expect(Timeline::Activity.new(json).verb).to eq("new_post") 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/actor_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), %w[spec_helper]) 2 | 3 | class User 4 | include Timeline::Actor 5 | 6 | attr_accessor :id 7 | end 8 | 9 | describe Timeline::Actor do 10 | describe "when included" do 11 | before { @user = User.new } 12 | 13 | it "defines a timeline association" do 14 | expect(@user).to respond_to :timeline 15 | end 16 | 17 | describe ".timeline" do 18 | subject { @user.timeline } 19 | 20 | it { should == [] } 21 | end 22 | end 23 | end -------------------------------------------------------------------------------- /spec/redis-test.conf: -------------------------------------------------------------------------------- 1 | # Redis configuration file example 2 | 3 | # Note on units: when memory size is needed, it is possible to specify 4 | # it in the usual form of 1k 5GB 4M and so forth: 5 | # 6 | # 1k => 1000 bytes 7 | # 1kb => 1024 bytes 8 | # 1m => 1000000 bytes 9 | # 1mb => 1024*1024 bytes 10 | # 1g => 1000000000 bytes 11 | # 1gb => 1024*1024*1024 bytes 12 | # 13 | # units are case insensitive so 1GB 1Gb 1gB are all the same. 14 | 15 | # By default Redis does not run as a daemon. Use 'yes' if you need it. 16 | # Note that Redis will write a pid file in /var/run/redis.pid when daemonized. 17 | daemonize yes 18 | 19 | # When running daemonized, Redis writes a pid file in /var/run/redis.pid by 20 | # default. You can specify a custom pid file location here. 21 | pidfile ./spec/redis-test.pid 22 | 23 | # Accept connections on the specified port, default is 6379. 24 | # If port 0 is specified Redis will not listen on a TCP socket. 25 | port 9736 26 | 27 | # If you want you can bind a single interface, if the bind option is not 28 | # specified all the interfaces will listen for incoming connections. 29 | # 30 | # bind 127.0.0.1 31 | 32 | # Specify the path for the unix socket that will be used to listen for 33 | # incoming connections. There is no default, so Redis will not listen 34 | # on a unix socket when not specified. 35 | # 36 | # unixsocket /tmp/redis.sock 37 | # unixsocketperm 755 38 | 39 | # Close the connection after a client is idle for N seconds (0 to disable) 40 | timeout 3000 41 | 42 | # TCP keepalive. 43 | # 44 | # If non-zero, use SO_KEEPALIVE to send TCP ACKs to clients in absence 45 | # of communication. This is useful for two reasons: 46 | # 47 | # 1) Detect dead peers. 48 | # 2) Take the connection alive from the point of view of network 49 | # equipment in the middle. 50 | # 51 | # On Linux, the specified value (in seconds) is the period used to send ACKs. 52 | # Note that to close the connection the double of the time is needed. 53 | # On other kernels the period depends on the kernel configuration. 54 | # 55 | # A reasonable value for this option is 60 seconds. 56 | tcp-keepalive 0 57 | 58 | # Specify the server verbosity level. 59 | # This can be one of: 60 | # debug (a lot of information, useful for development/testing) 61 | # verbose (many rarely useful info, but not a mess like the debug level) 62 | # notice (moderately verbose, what you want in production probably) 63 | # warning (only very important / critical messages are logged) 64 | loglevel debug 65 | 66 | # Specify the log file name. Also 'stdout' can be used to force 67 | # Redis to log on the standard output. Note that if you use standard 68 | # output for logging but daemonize, logs will be sent to /dev/null 69 | logfile stdout 70 | 71 | # To enable logging to the system logger, just set 'syslog-enabled' to yes, 72 | # and optionally update the other syslog parameters to suit your needs. 73 | # syslog-enabled no 74 | 75 | # Specify the syslog identity. 76 | # syslog-ident redis 77 | 78 | # Specify the syslog facility. Must be USER or between LOCAL0-LOCAL7. 79 | # syslog-facility local0 80 | 81 | # Set the number of databases. The default database is DB 0, you can select 82 | # a different one on a per-connection basis using SELECT where 83 | # dbid is a number between 0 and 'databases'-1 84 | databases 16 85 | 86 | ################################ SNAPSHOTTING ################################# 87 | # 88 | # Save the DB on disk: 89 | # 90 | # save 91 | # 92 | # Will save the DB if both the given number of seconds and the given 93 | # number of write operations against the DB occurred. 94 | # 95 | # In the example below the behaviour will be to save: 96 | # after 900 sec (15 min) if at least 1 key changed 97 | # after 300 sec (5 min) if at least 10 keys changed 98 | # after 60 sec if at least 10000 keys changed 99 | # 100 | # Note: you can disable saving at all commenting all the "save" lines. 101 | # 102 | # It is also possible to remove all the previously configured save 103 | # points by adding a save directive with a single empty string argument 104 | # like in the following example: 105 | # 106 | # save "" 107 | 108 | save 900 1 109 | save 300 10 110 | save 60 10000 111 | 112 | # By default Redis will stop accepting writes if RDB snapshots are enabled 113 | # (at least one save point) and the latest background save failed. 114 | # This will make the user aware (in an hard way) that data is not persisting 115 | # on disk properly, otherwise chances are that no one will notice and some 116 | # distater will happen. 117 | # 118 | # If the background saving process will start working again Redis will 119 | # automatically allow writes again. 120 | # 121 | # However if you have setup your proper monitoring of the Redis server 122 | # and persistence, you may want to disable this feature so that Redis will 123 | # continue to work as usually even if there are problems with disk, 124 | # permissions, and so forth. 125 | stop-writes-on-bgsave-error yes 126 | 127 | # Compress string objects using LZF when dump .rdb databases? 128 | # For default that's set to 'yes' as it's almost always a win. 129 | # If you want to save some CPU in the saving child set it to 'no' but 130 | # the dataset will likely be bigger if you have compressible values or keys. 131 | rdbcompression yes 132 | 133 | # Since version 5 of RDB a CRC64 checksum is placed at the end of the file. 134 | # This makes the format more resistant to corruption but there is a performance 135 | # hit to pay (around 10%) when saving and loading RDB files, so you can disable it 136 | # for maximum performances. 137 | # 138 | # RDB files created with checksum disabled have a checksum of zero that will 139 | # tell the loading code to skip the check. 140 | rdbchecksum yes 141 | 142 | # The filename where to dump the DB 143 | dbfilename dump.rdb 144 | 145 | # The working directory. 146 | # 147 | # The DB will be written inside this directory, with the filename specified 148 | # above using the 'dbfilename' configuration directive. 149 | # 150 | # The Append Only File will also be created inside this directory. 151 | # 152 | # Note that you must specify a directory here, not a file name. 153 | dir ./spec/ 154 | 155 | ################################# REPLICATION ################################# 156 | 157 | # Master-Slave replication. Use slaveof to make a Redis instance a copy of 158 | # another Redis server. Note that the configuration is local to the slave 159 | # so for example it is possible to configure the slave to save the DB with a 160 | # different interval, or to listen to another port, and so on. 161 | # 162 | # slaveof 163 | 164 | # If the master is password protected (using the "requirepass" configuration 165 | # directive below) it is possible to tell the slave to authenticate before 166 | # starting the replication synchronization process, otherwise the master will 167 | # refuse the slave request. 168 | # 169 | # masterauth 170 | 171 | # When a slave loses its connection with the master, or when the replication 172 | # is still in progress, the slave can act in two different ways: 173 | # 174 | # 1) if slave-serve-stale-data is set to 'yes' (the default) the slave will 175 | # still reply to client requests, possibly with out of date data, or the 176 | # data set may just be empty if this is the first synchronization. 177 | # 178 | # 2) if slave-serve-stale-data is set to 'no' the slave will reply with 179 | # an error "SYNC with master in progress" to all the kind of commands 180 | # but to INFO and SLAVEOF. 181 | # 182 | slave-serve-stale-data yes 183 | 184 | # You can configure a slave instance to accept writes or not. Writing against 185 | # a slave instance may be useful to store some ephemeral data (because data 186 | # written on a slave will be easily deleted after resync with the master) but 187 | # may also cause problems if clients are writing to it because of a 188 | # misconfiguration. 189 | # 190 | # Since Redis 2.6 by default slaves are read-only. 191 | # 192 | # Note: read only slaves are not designed to be exposed to untrusted clients 193 | # on the internet. It's just a protection layer against misuse of the instance. 194 | # Still a read only slave exports by default all the administrative commands 195 | # such as CONFIG, DEBUG, and so forth. To a limited extend you can improve 196 | # security of read only slaves using 'rename-command' to shadow all the 197 | # administrative / dangerous commands. 198 | slave-read-only yes 199 | 200 | # Slaves send PINGs to server in a predefined interval. It's possible to change 201 | # this interval with the repl_ping_slave_period option. The default value is 10 202 | # seconds. 203 | # 204 | # repl-ping-slave-period 10 205 | 206 | # The following option sets a timeout for both Bulk transfer I/O timeout and 207 | # master data or ping response timeout. The default value is 60 seconds. 208 | # 209 | # It is important to make sure that this value is greater than the value 210 | # specified for repl-ping-slave-period otherwise a timeout will be detected 211 | # every time there is low traffic between the master and the slave. 212 | # 213 | # repl-timeout 60 214 | 215 | # Disable TCP_NODELAY on the slave socket after SYNC? 216 | # 217 | # If you select "yes" Redis will use a smaller number of TCP packets and 218 | # less bandwidth to send data to slaves. But this can add a delay for 219 | # the data to appear on the slave side, up to 40 milliseconds with 220 | # Linux kernels using a default configuration. 221 | # 222 | # If you select "no" the delay for data to appear on the slave side will 223 | # be reduced but more bandwidth will be used for replication. 224 | # 225 | # By default we optimize for low latency, but in very high traffic conditions 226 | # or when the master and slaves are many hops away, turning this to "yes" may 227 | # be a good idea. 228 | repl-disable-tcp-nodelay no 229 | 230 | # The slave priority is an integer number published by Redis in the INFO output. 231 | # It is used by Redis Sentinel in order to select a slave to promote into a 232 | # master if the master is no longer working correctly. 233 | # 234 | # A slave with a low priority number is considered better for promotion, so 235 | # for instance if there are three slaves with priority 10, 100, 25 Sentinel will 236 | # pick the one wtih priority 10, that is the lowest. 237 | # 238 | # However a special priority of 0 marks the slave as not able to perform the 239 | # role of master, so a slave with priority of 0 will never be selected by 240 | # Redis Sentinel for promotion. 241 | # 242 | # By default the priority is 100. 243 | slave-priority 100 244 | 245 | ################################## SECURITY ################################### 246 | 247 | # Require clients to issue AUTH before processing any other 248 | # commands. This might be useful in environments in which you do not trust 249 | # others with access to the host running redis-server. 250 | # 251 | # This should stay commented out for backward compatibility and because most 252 | # people do not need auth (e.g. they run their own servers). 253 | # 254 | # Warning: since Redis is pretty fast an outside user can try up to 255 | # 150k passwords per second against a good box. This means that you should 256 | # use a very strong password otherwise it will be very easy to break. 257 | # 258 | # requirepass foobared 259 | 260 | # Command renaming. 261 | # 262 | # It is possible to change the name of dangerous commands in a shared 263 | # environment. For instance the CONFIG command may be renamed into something 264 | # hard to guess so that it will still be available for internal-use tools 265 | # but not available for general clients. 266 | # 267 | # Example: 268 | # 269 | # rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52 270 | # 271 | # It is also possible to completely kill a command by renaming it into 272 | # an empty string: 273 | # 274 | # rename-command CONFIG "" 275 | # 276 | # Please note that changing the name of commands that are logged into the 277 | # AOF file or transmitted to slaves may cause problems. 278 | 279 | ################################### LIMITS #################################### 280 | 281 | # Set the max number of connected clients at the same time. By default 282 | # this limit is set to 10000 clients, however if the Redis server is not 283 | # able to configure the process file limit to allow for the specified limit 284 | # the max number of allowed clients is set to the current file limit 285 | # minus 32 (as Redis reserves a few file descriptors for internal uses). 286 | # 287 | # Once the limit is reached Redis will close all the new connections sending 288 | # an error 'max number of clients reached'. 289 | # 290 | # maxclients 10000 291 | 292 | # Don't use more memory than the specified amount of bytes. 293 | # When the memory limit is reached Redis will try to remove keys 294 | # accordingly to the eviction policy selected (see maxmemmory-policy). 295 | # 296 | # If Redis can't remove keys according to the policy, or if the policy is 297 | # set to 'noeviction', Redis will start to reply with errors to commands 298 | # that would use more memory, like SET, LPUSH, and so on, and will continue 299 | # to reply to read-only commands like GET. 300 | # 301 | # This option is usually useful when using Redis as an LRU cache, or to set 302 | # an hard memory limit for an instance (using the 'noeviction' policy). 303 | # 304 | # WARNING: If you have slaves attached to an instance with maxmemory on, 305 | # the size of the output buffers needed to feed the slaves are subtracted 306 | # from the used memory count, so that network problems / resyncs will 307 | # not trigger a loop where keys are evicted, and in turn the output 308 | # buffer of slaves is full with DELs of keys evicted triggering the deletion 309 | # of more keys, and so forth until the database is completely emptied. 310 | # 311 | # In short... if you have slaves attached it is suggested that you set a lower 312 | # limit for maxmemory so that there is some free RAM on the system for slave 313 | # output buffers (but this is not needed if the policy is 'noeviction'). 314 | # 315 | # maxmemory 316 | 317 | # MAXMEMORY POLICY: how Redis will select what to remove when maxmemory 318 | # is reached. You can select among five behaviors: 319 | # 320 | # volatile-lru -> remove the key with an expire set using an LRU algorithm 321 | # allkeys-lru -> remove any key accordingly to the LRU algorithm 322 | # volatile-random -> remove a random key with an expire set 323 | # allkeys-random -> remove a random key, any key 324 | # volatile-ttl -> remove the key with the nearest expire time (minor TTL) 325 | # noeviction -> don't expire at all, just return an error on write operations 326 | # 327 | # Note: with any of the above policies, Redis will return an error on write 328 | # operations, when there are not suitable keys for eviction. 329 | # 330 | # At the date of writing this commands are: set setnx setex append 331 | # incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd 332 | # sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby 333 | # zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby 334 | # getset mset msetnx exec sort 335 | # 336 | # The default is: 337 | # 338 | # maxmemory-policy volatile-lru 339 | 340 | # LRU and minimal TTL algorithms are not precise algorithms but approximated 341 | # algorithms (in order to save memory), so you can select as well the sample 342 | # size to check. For instance for default Redis will check three keys and 343 | # pick the one that was used less recently, you can change the sample size 344 | # using the following configuration directive. 345 | # 346 | # maxmemory-samples 3 347 | 348 | ############################## APPEND ONLY MODE ############################### 349 | 350 | # By default Redis asynchronously dumps the dataset on disk. This mode is 351 | # good enough in many applications, but an issue with the Redis process or 352 | # a power outage may result into a few minutes of writes lost (depending on 353 | # the configured save points). 354 | # 355 | # The Append Only File is an alternative persistence mode that provides 356 | # much better durability. For instance using the default data fsync policy 357 | # (see later in the config file) Redis can lose just one second of writes in a 358 | # dramatic event like a server power outage, or a single write if something 359 | # wrong with the Redis process itself happens, but the operating system is 360 | # still running correctly. 361 | # 362 | # AOF and RDB persistence can be enabled at the same time without problems. 363 | # If the AOF is enabled on startup Redis will load the AOF, that is the file 364 | # with the better durability guarantees. 365 | # 366 | # Please check http://redis.io/topics/persistence for more information. 367 | 368 | appendonly no 369 | 370 | # The name of the append only file (default: "appendonly.aof") 371 | # appendfilename appendonly.aof 372 | 373 | # The fsync() call tells the Operating System to actually write data on disk 374 | # instead to wait for more data in the output buffer. Some OS will really flush 375 | # data on disk, some other OS will just try to do it ASAP. 376 | # 377 | # Redis supports three different modes: 378 | # 379 | # no: don't fsync, just let the OS flush the data when it wants. Faster. 380 | # always: fsync after every write to the append only log . Slow, Safest. 381 | # everysec: fsync only one time every second. Compromise. 382 | # 383 | # The default is "everysec", as that's usually the right compromise between 384 | # speed and data safety. It's up to you to understand if you can relax this to 385 | # "no" that will let the operating system flush the output buffer when 386 | # it wants, for better performances (but if you can live with the idea of 387 | # some data loss consider the default persistence mode that's snapshotting), 388 | # or on the contrary, use "always" that's very slow but a bit safer than 389 | # everysec. 390 | # 391 | # More details please check the following article: 392 | # http://antirez.com/post/redis-persistence-demystified.html 393 | # 394 | # If unsure, use "everysec". 395 | 396 | # appendfsync always 397 | appendfsync everysec 398 | # appendfsync no 399 | 400 | # When the AOF fsync policy is set to always or everysec, and a background 401 | # saving process (a background save or AOF log background rewriting) is 402 | # performing a lot of I/O against the disk, in some Linux configurations 403 | # Redis may block too long on the fsync() call. Note that there is no fix for 404 | # this currently, as even performing fsync in a different thread will block 405 | # our synchronous write(2) call. 406 | # 407 | # In order to mitigate this problem it's possible to use the following option 408 | # that will prevent fsync() from being called in the main process while a 409 | # BGSAVE or BGREWRITEAOF is in progress. 410 | # 411 | # This means that while another child is saving, the durability of Redis is 412 | # the same as "appendfsync none". In practical terms, this means that it is 413 | # possible to lose up to 30 seconds of log in the worst scenario (with the 414 | # default Linux settings). 415 | # 416 | # If you have latency problems turn this to "yes". Otherwise leave it as 417 | # "no" that is the safest pick from the point of view of durability. 418 | no-appendfsync-on-rewrite no 419 | 420 | # Automatic rewrite of the append only file. 421 | # Redis is able to automatically rewrite the log file implicitly calling 422 | # BGREWRITEAOF when the AOF log size grows by the specified percentage. 423 | # 424 | # This is how it works: Redis remembers the size of the AOF file after the 425 | # latest rewrite (if no rewrite has happened since the restart, the size of 426 | # the AOF at startup is used). 427 | # 428 | # This base size is compared to the current size. If the current size is 429 | # bigger than the specified percentage, the rewrite is triggered. Also 430 | # you need to specify a minimal size for the AOF file to be rewritten, this 431 | # is useful to avoid rewriting the AOF file even if the percentage increase 432 | # is reached but it is still pretty small. 433 | # 434 | # Specify a percentage of zero in order to disable the automatic AOF 435 | # rewrite feature. 436 | 437 | auto-aof-rewrite-percentage 100 438 | auto-aof-rewrite-min-size 64mb 439 | 440 | ################################ LUA SCRIPTING ############################### 441 | 442 | # Max execution time of a Lua script in milliseconds. 443 | # 444 | # If the maximum execution time is reached Redis will log that a script is 445 | # still in execution after the maximum allowed time and will start to 446 | # reply to queries with an error. 447 | # 448 | # When a long running script exceed the maximum execution time only the 449 | # SCRIPT KILL and SHUTDOWN NOSAVE commands are available. The first can be 450 | # used to stop a script that did not yet called write commands. The second 451 | # is the only way to shut down the server in the case a write commands was 452 | # already issue by the script but the user don't want to wait for the natural 453 | # termination of the script. 454 | # 455 | # Set it to 0 or a negative value for unlimited execution without warnings. 456 | lua-time-limit 5000 457 | 458 | ################################## SLOW LOG ################################### 459 | 460 | # The Redis Slow Log is a system to log queries that exceeded a specified 461 | # execution time. The execution time does not include the I/O operations 462 | # like talking with the client, sending the reply and so forth, 463 | # but just the time needed to actually execute the command (this is the only 464 | # stage of command execution where the thread is blocked and can not serve 465 | # other requests in the meantime). 466 | # 467 | # You can configure the slow log with two parameters: one tells Redis 468 | # what is the execution time, in microseconds, to exceed in order for the 469 | # command to get logged, and the other parameter is the length of the 470 | # slow log. When a new command is logged the oldest one is removed from the 471 | # queue of logged commands. 472 | 473 | # The following time is expressed in microseconds, so 1000000 is equivalent 474 | # to one second. Note that a negative number disables the slow log, while 475 | # a value of zero forces the logging of every command. 476 | slowlog-log-slower-than 10000 477 | 478 | # There is no limit to this length. Just be aware that it will consume memory. 479 | # You can reclaim memory used by the slow log with SLOWLOG RESET. 480 | slowlog-max-len 128 481 | 482 | ############################### ADVANCED CONFIG ############################### 483 | 484 | # Hashes are encoded using a memory efficient data structure when they have a 485 | # small number of entries, and the biggest entry does not exceed a given 486 | # threshold. These thresholds can be configured using the following directives. 487 | hash-max-ziplist-entries 512 488 | hash-max-ziplist-value 64 489 | 490 | # Similarly to hashes, small lists are also encoded in a special way in order 491 | # to save a lot of space. The special representation is only used when 492 | # you are under the following limits: 493 | list-max-ziplist-entries 512 494 | list-max-ziplist-value 64 495 | 496 | # Sets have a special encoding in just one case: when a set is composed 497 | # of just strings that happens to be integers in radix 10 in the range 498 | # of 64 bit signed integers. 499 | # The following configuration setting sets the limit in the size of the 500 | # set in order to use this special memory saving encoding. 501 | set-max-intset-entries 512 502 | 503 | # Similarly to hashes and lists, sorted sets are also specially encoded in 504 | # order to save a lot of space. This encoding is only used when the length and 505 | # elements of a sorted set are below the following limits: 506 | zset-max-ziplist-entries 128 507 | zset-max-ziplist-value 64 508 | 509 | # Active rehashing uses 1 millisecond every 100 milliseconds of CPU time in 510 | # order to help rehashing the main Redis hash table (the one mapping top-level 511 | # keys to values). The hash table implementation Redis uses (see dict.c) 512 | # performs a lazy rehashing: the more operation you run into an hash table 513 | # that is rehashing, the more rehashing "steps" are performed, so if the 514 | # server is idle the rehashing is never complete and some more memory is used 515 | # by the hash table. 516 | # 517 | # The default is to use this millisecond 10 times every second in order to 518 | # active rehashing the main dictionaries, freeing memory when possible. 519 | # 520 | # If unsure: 521 | # use "activerehashing no" if you have hard latency requirements and it is 522 | # not a good thing in your environment that Redis can reply form time to time 523 | # to queries with 2 milliseconds delay. 524 | # 525 | # use "activerehashing yes" if you don't have such hard requirements but 526 | # want to free memory asap when possible. 527 | activerehashing yes 528 | 529 | # The client output buffer limits can be used to force disconnection of clients 530 | # that are not reading data from the server fast enough for some reason (a 531 | # common reason is that a Pub/Sub client can't consume messages as fast as the 532 | # publisher can produce them). 533 | # 534 | # The limit can be set differently for the three different classes of clients: 535 | # 536 | # normal -> normal clients 537 | # slave -> slave clients and MONITOR clients 538 | # pubsub -> clients subcribed to at least one pubsub channel or pattern 539 | # 540 | # The syntax of every client-output-buffer-limit directive is the following: 541 | # 542 | # client-output-buffer-limit 543 | # 544 | # A client is immediately disconnected once the hard limit is reached, or if 545 | # the soft limit is reached and remains reached for the specified number of 546 | # seconds (continuously). 547 | # So for instance if the hard limit is 32 megabytes and the soft limit is 548 | # 16 megabytes / 10 seconds, the client will get disconnected immediately 549 | # if the size of the output buffers reach 32 megabytes, but will also get 550 | # disconnected if the client reaches 16 megabytes and continuously overcomes 551 | # the limit for 10 seconds. 552 | # 553 | # By default normal clients are not limited because they don't receive data 554 | # without asking (in a push way), but just after a request, so only 555 | # asynchronous clients may create a scenario where data is requested faster 556 | # than it can read. 557 | # 558 | # Instead there is a default limit for pubsub and slave clients, since 559 | # subscribers and slaves receive data in a push fashion. 560 | # 561 | # Both the hard or the soft limit can be disabled by setting them to zero. 562 | client-output-buffer-limit normal 0 0 0 563 | client-output-buffer-limit slave 256mb 64mb 60 564 | client-output-buffer-limit pubsub 32mb 8mb 60 565 | 566 | # Redis calls an internal function to perform many background tasks, like 567 | # closing connections of clients in timeot, purging expired keys that are 568 | # never requested, and so forth. 569 | # 570 | # Not all tasks are perforemd with the same frequency, but Redis checks for 571 | # tasks to perform accordingly to the specified "hz" value. 572 | # 573 | # By default "hz" is set to 10. Raising the value will use more CPU when 574 | # Redis is idle, but at the same time will make Redis more responsive when 575 | # there are many keys expiring at the same time, and timeouts may be 576 | # handled with more precision. 577 | # 578 | # The range is between 1 and 500, however a value over 100 is usually not 579 | # a good idea. Most users should use the default of 10 and raise this up to 580 | # 100 only in environments where very low latency is required. 581 | hz 10 582 | 583 | # When a child rewrites the AOF file, if the following option is enabled 584 | # the file will be fsync-ed every 32 MB of data generated. This is useful 585 | # in order to commit the file to the disk more incrementally and avoid 586 | # big latency spikes. 587 | aof-rewrite-incremental-fsync yes 588 | 589 | ################################## INCLUDES ################################### 590 | 591 | # Include one or more other config files here. This is useful if you 592 | # have a standard template that goes to all Redis server but also need 593 | # to customize a few per-server settings. Include files can include 594 | # other files, so use this wisely. 595 | # 596 | # include /path/to/local.conf 597 | # include /path/to/other.conf# Redis configuration file example 598 | 599 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), %w[.. lib redis-timeline])) 2 | dir = File.dirname(File.expand_path(__FILE__)) 3 | 4 | # 5 | # make sure we can run redis 6 | # 7 | 8 | if !system("which redis-server") 9 | puts '', "** can't find `redis-server` in your path" 10 | puts "** try running `sudo rake install`" 11 | abort '' 12 | end 13 | 14 | # 15 | # start our own redis when the tests start, 16 | # kill it when they end 17 | # 18 | 19 | at_exit do 20 | next if $! 21 | 22 | exit_code = RSpec::Core::Runner.autorun 23 | 24 | pid = `ps -A -o pid,command | grep [r]edis-test`.split(" ")[0] 25 | puts "Killing test redis server..." 26 | `rm -f #{dir}/dump.rdb` 27 | Process.kill("KILL", pid.to_i) 28 | exit exit_code 29 | end 30 | 31 | puts "Starting redis for testing at localhost:9736..." 32 | `redis-server #{dir}/redis-test.conf` 33 | Timeline.redis = 'localhost:9736' 34 | -------------------------------------------------------------------------------- /spec/stdout: -------------------------------------------------------------------------------- 1 | [97466] 18 Sep 14:58:15.166 - DB 0: 5 keys (0 volatile) in 8 slots HT. 2 | [97466] 18 Sep 14:58:15.167 - 0 clients connected (0 slaves), 1006848 bytes in use 3 | [97466] 18 Sep 14:58:20.205 - DB 0: 5 keys (0 volatile) in 8 slots HT. 4 | [97466] 18 Sep 14:58:20.205 - 0 clients connected (0 slaves), 1006848 bytes in use 5 | [97466] 18 Sep 14:58:25.231 - DB 0: 5 keys (0 volatile) in 8 slots HT. 6 | [97466] 18 Sep 14:58:25.231 - 0 clients connected (0 slaves), 1006848 bytes in use 7 | -------------------------------------------------------------------------------- /spec/timeline_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), %w[spec_helper]) 2 | 3 | describe Timeline do 4 | it("can set a redis instance") { expect(Timeline).to respond_to(:redis=) } 5 | it("has a namespace, timeline") { expect(Timeline.redis.namespace).to eq(:timeline) } 6 | 7 | it "sets the namespace through a url-like string" do 8 | Timeline.redis = 'localhost:9736/namespace' 9 | expect(Timeline.redis.namespace).to eq('namespace') 10 | end 11 | end 12 | 13 | -------------------------------------------------------------------------------- /spec/track_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), %w[spec_helper]) 2 | 3 | require 'active_model' 4 | 5 | class Post 6 | extend ActiveModel::Callbacks 7 | 8 | define_model_callbacks :create 9 | attr_accessor :id, :to_param, :creator_id, :name 10 | 11 | include Timeline::Track 12 | track :new_post 13 | 14 | def initialize(options={}) 15 | @creator_id = options.delete :creator_id 16 | @name = options.delete :name 17 | end 18 | 19 | def save 20 | run_callbacks :create 21 | true 22 | end 23 | 24 | def creator 25 | User.find(creator_id) 26 | end 27 | 28 | def to_s 29 | name 30 | end 31 | end 32 | 33 | class Comment 34 | extend ActiveModel::Callbacks 35 | 36 | define_model_callbacks :create 37 | attr_accessor :id, :creator_id, :body 38 | 39 | include Timeline::Track 40 | 41 | track :new_comment, object: [:post_name, :post_id, :body], mentionable: :body 42 | 43 | def initialize(options={}) 44 | @creator_id = options.delete :creator_id 45 | @body = options.delete :body 46 | end 47 | 48 | def save 49 | run_callbacks :create 50 | true 51 | end 52 | 53 | def post_id 54 | 1 55 | end 56 | 57 | def post_name 58 | "My Post" 59 | end 60 | 61 | def creator 62 | User.find(creator_id) 63 | end 64 | 65 | def to_s 66 | "Comment" 67 | end 68 | end 69 | 70 | class User 71 | include Timeline::Actor 72 | attr_accessor :id, :to_param, :username 73 | 74 | def initialize(options={}) 75 | @id = options.delete :id 76 | @username = options.delete :username 77 | end 78 | 79 | class << self 80 | def find user_id 81 | User.new(id: user_id) 82 | end 83 | 84 | def find_by_username username 85 | User.new(username: username) 86 | end 87 | end 88 | end 89 | 90 | describe Timeline::Track do 91 | let(:creator) { User.new(id: 1, username: "first_user") } 92 | let(:post) { Post.new(creator_id: creator.id, name: "New post") } 93 | let(:comment) { Comment.new(creator_id: creator.id, id: 1) } 94 | 95 | describe "included in an ActiveModel-compliant class" do 96 | it "tracks on create by default" do 97 | expect(post).to receive(:track_new_post_after_create) 98 | post.save 99 | end 100 | 101 | it "uses the creator as the actor by default" do 102 | expect(post).to receive(:creator).and_return(double("User", id: 1, to_param: "1", followers: [])) 103 | post.save 104 | end 105 | 106 | it "adds the activity to the global timeline set" do 107 | post.save 108 | expect(creator.timeline(:global).last).to be_kind_of(Timeline::Activity) 109 | end 110 | 111 | it "adds the activity to the actor's timeline" do 112 | post.save 113 | expect(creator.timeline.last).to be_kind_of(Timeline::Activity) 114 | end 115 | 116 | it "cc's the actor's followers by default" do 117 | follower = User.new(id: 2) 118 | expect_any_instance_of(User).to receive(:followers).and_return([follower]) 119 | post.save 120 | expect(follower.timeline.last.verb).to eq("new_post") 121 | expect(follower.timeline.last.actor.id).to eq(1) 122 | end 123 | end 124 | 125 | describe "with extra_fields" do 126 | it "stores the extra fields in the timeline" do 127 | comment.save 128 | expect(creator.timeline.first.object).to respond_to :post_id 129 | end 130 | end 131 | 132 | describe "tracking mentions" do 133 | it "adds to a user's mentions timeline" do 134 | allow(User).to receive(:find_by_username).and_return(creator) 135 | Comment.new(creator_id: creator.id, body: "@first_user should see this").save 136 | expect(creator.timeline(:mentions).first.object.body).to eq("@first_user should see this") 137 | end 138 | end 139 | end 140 | --------------------------------------------------------------------------------