├── .rspec ├── lib ├── hey_you │ └── version.rb └── hey_you.rb ├── Gemfile ├── .gitignore ├── bin ├── setup └── console ├── Rakefile ├── .travis.yml ├── spec ├── fixtures │ ├── once.rb │ ├── twice.rb │ └── fixture_helper.rb ├── spec_helper.rb └── hey_you_spec.rb ├── LICENSE.txt ├── hey_you.gemspec ├── CODE_OF_CONDUCT.md └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /lib/hey_you/version.rb: -------------------------------------------------------------------------------- 1 | class HeyYou 2 | VERSION = "0.1.1" 3 | end 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in hey_you.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | bundle install 6 | 7 | # Do any other automated setup that you need to do here 8 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | task :test => :spec 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.2.3 4 | before_install: gem install bundler -v 1.10.6 5 | 6 | before_script: 7 | - psql -c 'create database hey_you_test;' -U postgres -------------------------------------------------------------------------------- /spec/fixtures/once.rb: -------------------------------------------------------------------------------- 1 | require_relative 'fixture_helper.rb' 2 | 3 | HeyYou.new(channel: testing_key("once"), sleep: 0.5).listen do |payload| 4 | puts "Got payload: #{payload}" 5 | end 6 | 7 | puts "I ran" 8 | 9 | sleep 10 10 | -------------------------------------------------------------------------------- /spec/fixtures/twice.rb: -------------------------------------------------------------------------------- 1 | require_relative 'fixture_helper.rb' 2 | 3 | HeyYou.new(channel: testing_key("twice_1"), sleep: 0.5).listen do |payload| 4 | puts "Got payload: #{payload}" 5 | end 6 | 7 | 8 | HeyYou.new(channel: testing_key("twice_2"), sleep: 0.5).listen do |payload| 9 | puts "Got payload: #{payload}" 10 | end 11 | 12 | puts "I ran" 13 | 14 | sleep 10 15 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "hey_you" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /spec/fixtures/fixture_helper.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | require 'hey_you' 3 | 4 | STDOUT.sync = true 5 | 6 | begin 7 | ActiveRecord::Base.establish_connection( 8 | adapter: 'postgresql', 9 | database: 'hey_you_test' 10 | ) 11 | ActiveRecord::Base.connection.raw_connection.exec("select 1") 12 | rescue ActiveRecord::NoDatabaseError => e 13 | msg = "\nCreate a database to continue `$ createdb hey_you_test` \n" + e.message 14 | raise e, msg 15 | end 16 | 17 | def testing_key(base = nil) 18 | [base, ENV["TRAVIS_BUILD_ID"] , ENV["TRAVIS_JOB_ID"] ].compact.join("_") 19 | end 20 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'hey_you' 3 | require 'fixtures/fixture_helper' 4 | 5 | def new_log_file 6 | Pathname.new(Tempfile.new(["hey_you", ".log"])) 7 | end 8 | 9 | def fixture_path(name = nil) 10 | path = Pathname.new(File.expand_path("../fixtures", __FILE__)) 11 | path = path.join(name) if name 12 | path 13 | end 14 | 15 | def expect_log_has_count(log:, count:, msg: "Got payload:") 16 | contents = File.read(log) 17 | actual = contents.scan(msg).count 18 | expect(actual).to eq(count), "Expected #{msg.inspect} to occur #{count} times but was #{ actual.inspect } in:\n#{ contents.inspect }" 19 | end 20 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 schneems 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. 22 | -------------------------------------------------------------------------------- /hey_you.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'hey_you/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "hey_you" 8 | spec.version = HeyYou::VERSION 9 | spec.authors = ["schneems"] 10 | spec.email = ["richard.schneeman@gmail.com"] 11 | 12 | spec.summary = %q{Use postgres to trigger custom events} 13 | spec.description = %q{Trigger custom events with postgres} 14 | spec.homepage = "https://github.com/schneems/hey_you" 15 | spec.license = "MIT" 16 | 17 | 18 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 19 | spec.bindir = "exe" 20 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 21 | spec.require_paths = ["lib"] 22 | 23 | spec.add_development_dependency "pg", ">= 0.15" 24 | spec.add_development_dependency "activerecord", ">= 3" 25 | spec.add_development_dependency "bundler", "~> 1.10" 26 | spec.add_development_dependency "rake", "~> 10.0" 27 | spec.add_development_dependency "rspec" 28 | end 29 | -------------------------------------------------------------------------------- /spec/hey_you_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe HeyYou do 4 | it 'has a version number' do 5 | expect(HeyYou::VERSION).not_to be nil 6 | end 7 | 8 | it "only gets messages for specified channel" do 9 | begin 10 | log = new_log_file 11 | pid = Process.spawn("bundle exec ruby #{ fixture_path("twice.rb") } >> #{log}") 12 | sleep 4 # wait for listen to happen before we can notify 13 | HeyYou.new(channel: testing_key("twice_1")).notify("yoyoyo") 14 | HeyYou.new(channel: testing_key("twice_2")).notify("supsupsup") 15 | Process.wait(pid) 16 | 17 | expect_log_has_count(log: log, count: 1, msg: "yoyoyo") 18 | expect_log_has_count(log: log, count: 1, msg: "supsupsup") 19 | ensure 20 | FileUtils.remove_entry_secure log 21 | end 22 | end 23 | 24 | it "sends different processes messages" do 25 | begin 26 | log = new_log_file 27 | pid = Process.spawn("bundle exec ruby #{ fixture_path("once.rb") } >> #{log}") 28 | sleep 4 # wait for listen to happen before we can notify 29 | HeyYou.new(channel: testing_key("once")).notify("yoyoyo") 30 | Process.wait(pid) 31 | 32 | expect_log_has_count(log: log, count: 1, msg: "yoyoyo") 33 | ensure 34 | FileUtils.remove_entry_secure log 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/hey_you.rb: -------------------------------------------------------------------------------- 1 | require "hey_you/version" 2 | 3 | class HeyYou 4 | DEFAULT_CHANNEL = "hey_you_default_channel" 5 | DEFAULT_CONNECTION_CONNECTOR = Proc.new do 6 | if defined?(DEFAULT_CONNECTION) 7 | DEFAULT_CONNECTION 8 | elsif defined?(ActiveRecord::Base) 9 | ActiveRecord::Base.connection.raw_connection 10 | else 11 | false 12 | end 13 | end 14 | 15 | attr_reader :channel 16 | def initialize(ttl: 0.1, sleep: 60, channel: DEFAULT_CHANNEL, connection: DEFAULT_CONNECTION_CONNECTOR.call) 17 | raise "The new method does not take a block, perhaps you meant to call the listen method?" if block_given? 18 | @ttl = ttl 19 | @sleep = sleep 20 | @channel = channel 21 | @raw_connection = connection 22 | end 23 | 24 | def listen(&block) 25 | raise "No block given" unless block 26 | @raw_connection.exec "LISTEN #{channel}" 27 | 28 | @thread = Thread.new do 29 | while true do 30 | sleep @sleep 31 | 32 | @raw_connection.wait_for_notify(@ttl) do |channel_sent, pid, payload| 33 | block.call(payload) if channel_sent == self.channel 34 | end 35 | end 36 | end 37 | end 38 | alias :watch :listen 39 | 40 | def notify(payload) 41 | message = "NOTIFY #{channel}, '#{payload}'" 42 | puts message 43 | @raw_connection.exec(message) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 12 | 13 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HeyYou 2 | 3 | [![Build Status](https://travis-ci.org/schneems/hey_you.svg?branch=master)](https://travis-ci.org/schneems/hey_you) 4 | 5 | Use postgres to [LISTEN/NOTIFY](http://www.postgresql.org/docs/9.1/static/sql-notify.html) to trigger custom events. 6 | 7 | ## Why? 8 | 9 | Let's say you've got a fleet of machines running code, you only want to trigger a custom event on all of them or one of them. With `HeyYou` it is super easy. What kind of event? Maybe it is for debugging or maybe it is for housekeeping. For example you could take a heap dump off of a running server and upload it to S3 using this library. 10 | 11 | ## Installation 12 | 13 | Add this line to your application's Gemfile: 14 | 15 | ```ruby 16 | gem 'hey_you' 17 | ``` 18 | 19 | And then execute: 20 | 21 | $ bundle 22 | 23 | Or install it yourself as: 24 | 25 | $ gem install hey_you 26 | 27 | ## Usage 28 | 29 | Somewhere in your code you can run: 30 | 31 | ```ruby 32 | HeyYou.new.listen do |payload| 33 | puts "Hey I got a message: #{payload}" 34 | end 35 | ``` 36 | 37 | This will spin up a new thread and periodically check for a notification, this won't block execution by default. 38 | 39 | Then in another console, you can run: 40 | 41 | ```ruby 42 | HeyYou.new.notify("sup") 43 | ``` 44 | 45 | When your listening process gets the message it will print out 46 | 47 | ```ruby 48 | "Hey I got a message: sup" 49 | ``` 50 | 51 | By default all listens and notifies happen on the same channel. If you want you can specify a different channel 52 | 53 | ```ruby 54 | HeyYou.new(channel: "new_channel_name").listen do |payload| 55 | puts "Hey I got a message: #{payload}" 56 | end 57 | ``` 58 | 59 | It is important to note that you need to notify against the same name. 60 | 61 | ```ruby 62 | HeyYou.new(channel: "new_channel_name").notify("sup") 63 | ``` 64 | 65 | By default the background thread wakes up every 60 seconds and waits 0.1 seconds to see if there is a message. You can customize this behavior using `sleep` and `ttl`. So to have it check every 10 seconds, and not wait at all you could run 66 | 67 | ```ruby 68 | HeyYou.new(ttl: 1, sleep: 10).listen do |payload| 69 | puts "Hey I got a message: #{payload}" 70 | end 71 | ``` 72 | 73 | A lower sleep will decrease delay between sending and receiving messages but will decrease overall program performance. 74 | 75 | ## Connection 76 | 77 | By default HeyYou assumes you're using Active Record and already have a connection configured. If you want to use a different ORM, you'll need to provide HeyYou with an object that responds to `exec` that executes arbitrary SQL and a method `wait_for_notify` that accepts `ttl`, `sleep` and takes a block. 78 | 79 | You can configure your connection manually 80 | 81 | ```ruby 82 | HeyYou.new(connection: my_custom_object) 83 | ``` 84 | 85 | Or you can set it globally 86 | 87 | ```ruby 88 | HeyYou::DEFAULT_CONNECTION = my_custom_object 89 | ``` 90 | 91 | ## Development 92 | 93 | 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. 94 | 95 | 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). 96 | 97 | ## Contributing 98 | 99 | Bug reports and pull requests are welcome on GitHub at https://github.com/schneems/hey_you. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct. 100 | 101 | 102 | ## License 103 | 104 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 105 | 106 | --------------------------------------------------------------------------------