* VRFS-801 - almost working on my dev machine

This commit is contained in:
Seth Call 2014-01-11 04:57:07 +00:00
parent 230a4d3f64
commit d89ce4c140
35 changed files with 885 additions and 126 deletions

View File

@ -50,6 +50,8 @@ gem 'postgres-copy', '0.6.0'
gem 'aws-sdk', '1.29.1'
gem 'bugsnag'
gem 'resque'
gem 'resque-retry'
gem 'resque-failed-job-mailer'
gem 'eventmachine', '1.0.3'
gem 'amqp', '0.9.8'

View File

@ -1,4 +1,6 @@
require 'resque/server'
require 'resque-retry'
require 'resque-retry/server'
JamAdmin::Application.routes.draw do

View File

@ -86,3 +86,4 @@ music_sessions_have_claimed_recording.sql
discardable_recorded_tracks2.sql
icecast.sql
home_page_promos.sql
mix_job_watch.sql

View File

@ -0,0 +1,5 @@
-- add some columns to help understand mix job completion
ALTER TABLE mixes ADD COLUMN completed BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE mixes ADD COLUMN error_count INTEGER NOT NULL DEFAULT 0;
ALTER TABLE mixes ADD COLUMN error_reason TEXT;
ALTER TABLE mixes ADD COLUMN error_detail TEXT;

View File

@ -24,9 +24,12 @@ gem 'carrierwave'
gem 'aasm', '3.0.16'
gem 'devise', '>= 1.1.2'
gem 'postgres-copy'
gem 'resque'
gem 'geokit-rails'
gem 'postgres_ext'
gem 'resque'
gem 'resque-retry'
gem 'resque-failed-job-mailer' #, :path => "/Users/seth/workspace/resque_failed_job_mailer"
gem 'oj'
if devenv
gem 'jam_db', :path=> "../db/target/ruby_package"
@ -43,6 +46,7 @@ group :test do
gem 'database_cleaner', '0.7.0'
gem 'rest-client'
gem 'faker'
gem 'resque_spec'
end
# Specify your gem's dependencies in jam_ruby.gemspec

View File

@ -1,16 +1,15 @@
module JamRuby
class Profanity
@@dictionary_file = File.join('config/profanity.yml')
@@dictionary_file = File.join(File.dirname(__FILE__), '../../..', 'config/profanity.yml')
@@dictionary = nil
def self.dictionary
if File.exists? @@dictionary_file
@@dictionary ||= YAML.load_file(@@dictionary_file)
else
@@dictionary = []
end
@@dictionary
@@dictionary ||= load_dictionary
end
def self.load_dictionary
YAML.load_file(@@dictionary_file)
end
def self.check_word(word)

View File

@ -8,6 +8,8 @@ module JamRuby
@@def_opts = { :expires => 3600 * 24, :secure => true } # 24 hours from now
S3_PREFIX = 's3://'
def initialize(aws_bucket, aws_key, aws_secret)
@aws_bucket = aws_bucket
@s3 = AWS::S3.new(:access_key_id => aws_key, :secret_access_key => aws_secret)
@ -16,7 +18,11 @@ module JamRuby
end
def s3_url(filename)
"s3://#{@aws_bucket}/#{filename}"
"#{S3_PREFIX}#{@aws_bucket}/#{filename}"
end
def s3_url?(filename)
filename.start_with? S3_PREFIX
end
def url(filename, options = @@def_opts)
@ -34,8 +40,12 @@ module JamRuby
}
end
def sign_url(path, options = @@def_opts)
s3_bucket.objects[path].url_for(:read, options).to_s
def sign_url(key, options = @@def_opts, operation = :read)
s3_bucket.objects[key].url_for(operation, options).to_s
end
def presigned_post(key, options = @@def_opts)
s3_bucket.objects[key].presigned_post(options)
end
def multipart_upload_start(upload_filename)
@ -58,6 +68,10 @@ module JamRuby
s3_bucket.objects[filename].delete
end
def upload(key, filename)
s3_bucket.objects[key].write(:file => filename)
end
def delete_folder(folder)
s3_bucket.objects.with_prefix(folder).delete_all
end

View File

@ -11,39 +11,32 @@ module JamRuby
def self.schedule(recording, manifest)
raise if recording.nil?
raise if manifest.nil?
mix = Mix.new
mix = Mix.new
mix.recording = recording
mix.manifest = manifest
mix.manifest = manifest.to_json
mix.save
mix.url = construct_filename(recording.id, mix.id)
mix.save
mix
end
def self.next(mix_server)
# First check if there are any mixes started so long ago that we want to re-run them
Mix.where("completed_at IS NULL AND started_at < ?", Time.now - MAX_MIX_TIME).each do |mix|
# FIXME: This should probably throw some kind of log, since it means something went wrong
mix.started_at = nil
mix.mix_server = nil
mix.save
if mix.save
Resque.enqueue(AudioMixer, mix.id, mix.sign_put)
end
mix = Mix.where(:started_at => nil).limit(1).first
return nil if mix.nil?
mix.started_at = Time.now
mix.mix_server = mix_server
mix.save
mix
end
def errored(reason, detail)
self.error_reason = reason
self.error_detail = detail
self.error_count = self.error_count + 1
save
end
def finish(length, md5)
self.completed_at = Time.now
self.length = length
self.md5 = md5
self.completed = true
save
end
@ -52,12 +45,16 @@ module JamRuby
end
def is_completed
!completed_at.nil?
completed
end
def sign_url
def sign_url(expiration_time = 120)
# expire link in 1 minute--the expectation is that a client is immediately following this link
s3_manager.sign_url(filename, {:expires => 120 , :response_content_type => 'audio/ogg'})
s3_manager.sign_url(filename, {:expires => expiration_time, :response_content_type => 'audio/ogg', :secure => false})
end
def sign_put(expiration_time = 3600 * 24)
s3_manager.sign_url(filename, {:expires => expiration_time, :content_type => 'audio/ogg', :secure => false}, :put)
end
private

View File

@ -91,9 +91,8 @@ module JamRuby
recorded_track
end
def sign_url
# expire link in 1 minute--the expectation is that a client is immediately following this link
s3_manager.sign_url(url, {:expires => 120, :response_content_type => 'audio/ogg'})
def sign_url(expiration_time = 120)
s3_manager.sign_url(url, {:expires => expiration_time, :response_content_type => 'audio/ogg', :secure => false})
end
def upload_start(length, md5)

View File

@ -268,7 +268,7 @@ module JamRuby
return unless recorded_track.fully_uploaded
end
self.mixes << Mix.schedule(self, base_mix_manifest.to_json)
self.mixes << Mix.schedule(self, base_mix_manifest)
save
end
@ -298,12 +298,13 @@ module JamRuby
mix_params = []
recorded_tracks.each do |recorded_track|
return nil unless recorded_track.fully_uploaded
manifest["files"] << { "url" => recorded_track.url, "codec" => "vorbis", "offset" => 0 }
manifest["files"] << { "filename" => recorded_track.sign_url(60 * 60 * 24), "codec" => "vorbis", "offset" => 0 }
mix_params << { "level" => 100, "balance" => 0 }
end
manifest["timeline"] << { "timestamp" => 0, "mix" => mix_params }
manifest["timeline"] << { "timestamp" => duration, "end" => true }
manifest["output"] = { "codec" => "vorbis" }
manifest["recording_id"] = self.id
manifest
end

View File

@ -1,25 +1,36 @@
require 'json'
require 'resque'
require 'resque-retry'
require 'net/http'
require 'digest/md5'
module JamRuby
@queue = :audiomixer
class AudioMixer
@queue = :audiomixer
#extend Resque::Plugins::Retry
@@log = Logging.logger[AudioMixer]
def self.perform(manifest)
audiomixer = AudioMixer.new
audiomixer.run(manifest)
attr_accessor :mix_id, :manifest, :manifest_file, :output_filename, :error_out_filename, :postback_output_url,
:error_reason, :error_detail
def self.perform(mix_id, postback_output_url)
audiomixer = AudioMixer.new()
audiomixer.postback_output_url = postback_output_url
audiomixer.mix_id = mix_id
audiomixer.run
end
def initialize
#@s3_manager = S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key)
end
def validate
raise "no manifest specified" unless @manifest
raise "no files specified" if !@manifest[:files] || @manifest[:files].length == 0
@manifest[:files].each do |file|
@ -35,41 +46,209 @@ module JamRuby
raise "no output specified" unless @manifest[:output]
raise "no output codec specified" unless @manifest[:output][:codec]
raise "no output filename specified" unless @manifest[:output][:filename]
raise "no timeline specified" unless @manifest[:timeline]
raise "no recording_id specified" unless @manifest[:recording_id]
raise "no mix_id specified" unless @manifest[:mix_id]
end
raise "no timeline specified" if !@manifest[:timeline] || @manifest[:timeline].length == 0
@manifest[:timeline].each do |entry|
def fetch_audio_files
@manifest[:files].each do |file|
filename = file[:filename]
if filename.start_with? "http"
# fetch it from wherever, put it somewhere on disk, and replace filename in the file parameter with the local disk one
download_filename = Dir::Tmpname.make_tmpname(["#{Dir.tmpdir}audiomixer-file", '.ogg'], nil)
uri = URI(filename)
open download_filename, 'w' do |io|
begin
Net::HTTP.start(uri.host, uri.port) do |http|
request = Net::HTTP::Get.new uri
http.request request do |response|
response_code = response.code.to_i
unless response_code >= 200 && response_code <= 299
raise "bad status code: #{response_code}. body: #{response.body}"
end
response.read_body do |chunk|
io.write chunk
end
end
end
rescue Exception => e
@error_reason = "unable to download"
@error_detail = "url #{filename}, error=#{e}"
raise e
end
end
filename = download_filename
file[:filename] = download_filename
end
raise "no file located at: #{filename}" unless File.exist? filename
end
end
def fetch_audio_files
def prepare
# make sure there is a place to write the .ogg mix
prepare_output
# make sure there is a place to write the error_out json file (if audiomixer fails this is needed)
prepare_error_out
prepare_manifest
end
def run(manifest)
# write the manifest object to file, to pass into audiomixer
def prepare_manifest
@manifest = symbolize_keys(manifest)
validate
fetch_audio_files
manifest_file = Dir::Tmpname.make_tmpname "/var/tmp/audiomixer/manifest-#{@manifest['recordingId']}", nil
File.open(manifest_file,"w") do |f|
@manifest_file = Dir::Tmpname.make_tmpname( ["#{Dir.tmpdir}audiomixer-manifest-#{@manifest['recording_id']}", '.json'], nil)
File.open(@manifest_file,"w") do |f|
f.write(@manifest.to_json)
end
#{"files": [{"codec": "vorbis", "offset": 0, "filename": "TPD - bass.flac-stereo.ogg"},
# {"codec": "vorbis", "offset": 0, "filename": "TPD - bg vox.flac-stereo.ogg"},
# {"codec": "vorbis", "offset": 0, "filename": "TPD - drums.flac-stereo.ogg"},
# {"codec": "vorbis", "offset": 0, "filename": "TPD - guitars.flac-stereo.ogg"},
# {"codec": "vorbis", "offset": 0, "filename": "TPD - lead vox.flac-stereo.ogg"}],
# "output": {"codec": "vorbis", "filename": "mix.ogg"},
# "timeline":
# [{"timestamp": 0, "mix": [{"balance": 0, "level": 100}, {"balance": 0, "level": 100}, {"balance": 0, "level": 100}, {"balance": 0, "level": 100}, {"balance": 0, "level": 100}]}]}
@@log.debug("manifest: #{@manifest}")
end
# make a suitable location to store the output mix, and pass the chosen filepath into the manifest
def prepare_output
@output_filename = Dir::Tmpname.make_tmpname( ["#{Dir.tmpdir}audiomixer-output-#{@manifest['recording_id']}", '.ogg'], nil)
# update manifest so that audiomixer writes here
@manifest[:output][:filename] = @output_filename
@@log.debug("output ogg file: #{@output_filename}")
end
# make a suitable location to store an output error file, which will be populated on failure to help diagnose problems.
def prepare_error_out
@error_out_filename = Dir::Tmpname.make_tmpname( ["#{Dir.tmpdir}audiomixer-error-out-#{@manifest['recording_id']}", '.ogg'], nil)
# update manifest so that audiomixer writes here
@manifest[:error_out] = @error_out_filename
@@log.debug("error_out: #{@error_out_filename}")
end
# read in and parse the error file that audiomixer pops out
def parse_error_out
error_out_data = File.read(@error_out_filename)
begin
@error_out = JSON.parse(error_out_data)
rescue
@error_reason = "unable-parse-error-out"
@@log.error("unable to parse error_out_data: #{error_out_data} from error_out: #{@error_out_filename}")
end
@error_reason = @error_out[:reason]
@error_reason = "unspecified-reason" unless @error_reason
@error_detail = @error_detail[:detail]
end
def postback
raise "no output file after mix" unless File.exist? @output_filename
@@log.debug("posting mix to #{@postback_output_url}")
uri = URI.parse(@postback_output_url)
http = Net::HTTP.new(uri.host, uri.port)
request = Net::HTTP::Put.new(uri.request_uri)
response = nil
File.open(@output_filename,"r") do |f|
request.body_stream=f
request["Content-Type"] = "audio/ogg"
request.add_field('Content-Length', File.size(@output_filename))
response = http.request(request)
end
response_code = response.code.to_i
unless response_code >= 200 && response_code <= 299
@error_reason = "postback-mix-to-s3"
raise "unable to put to url: #{@postback_output_url}, status: #{response.code}, body: #{response.body}"
end
end
def post_success(mix)
raise "no output file after mix" unless File.exist? @output_filename
length = File.size(@output_filename)
md5 = Digest::MD5.new
File.open(@output_filename, 'rb').each {|line| md5.update(line)}
mix.finish(length, md5.to_s)
end
def post_error(mix, e)
begin
# if error_reason is null, assume this is an unhandled error
unless @error_reason
@error_reason = "unhandled-job-exception"
@error_detail = e.to_s
end
mix.errored(error_reason, error_detail)
rescue
@@log.error "unable to post back to the database the error"
end
end
def run
@@log.info("audiomixer job starting. mix_id #{mix_id}")
mix = Mix.find(mix_id)
begin
@manifest = symbolize_keys(JSON.parse(mix.manifest))
@manifest[:mix_id] = mix_id # slip in the mix_id so that the job can add it to the ogg comments
# sanity check the manifest
validate
# if http files are specified, bring them local
fetch_audio_files
# write the manifest to file, so that it can be passed to audiomixer as an filepath argument
prepare
result = execute(@manifest_file)
if result
if $? == 0
postback
post_success(mix)
@@log.info("audiomixer job successful. mix_id #{mix_id}")
else
parse_error_out
error_msg = "audiomixer job failed status=#{$?} error_reason=#{error_reason} error_detail=#{error_detail}"
@@log.info(error_msg)
raise error_msg
end
else
@@log.error("unable to find audiomixer")
error_msg = "audiomixer job failed status=#{$?} error_reason=#{error_reason} error_detail=#{error_detail}"
@@log.info(error_msg)
@error_reason = "unable-find-appmixer"
raise error_msg
end
rescue Exception => e
post_error(mix, e)
raise
end
end
def manifest=(value)
@manifest = symbolize_keys(value)
end
private
def execute(manifest_file)
audiomixer_cmd = "#{APP_CONFIG.audiomixer_path} #{manifest_file}"
@@log.debug("executing #{audiomixer_cmd}")

View File

@ -2,6 +2,7 @@ require 'spec_helper'
describe Mix do
before do
stub_const("APP_CONFIG", app_config)
@user = FactoryGirl.create(:user)
@connection = FactoryGirl.create(:connection, :user => @user)
@instrument = FactoryGirl.create(:instrument, :description => 'a great instrument')
@ -13,12 +14,13 @@ describe Mix do
@recording.stop
@recording.claim(@user, "name", "description", Genre.first, true, true)
@recording.errors.any?.should be_false
@mix = Mix.schedule(@recording, "{}")
@mix = Mix.schedule(@recording, {})
@mix.reload
end
it "should create a mix for a user's recording properly" do
@mix.recording_id.should == @recording.id
@mix.manifest.should == "{}"
@mix.manifest.should == {}.to_json
@mix.mix_server.should be_nil
@mix.started_at.should be_nil
@mix.completed_at.should be_nil
@ -33,15 +35,6 @@ describe Mix do
expect { @mix2 = Mix.schedule(Recording.find('lskdjflsd')) }.to raise_error
end
it "should return a mix when the cron asks for it" do
this_mix = Mix.next("server")
this_mix.id.should == @mix.id
@mix.reload
@mix.started_at.should_not be_nil
@mix.mix_server.should == "server"
@mix.completed_at.should be_nil
end
it "should record when a mix has finished" do
Mix.find(@mix.id).finish(10000, "md5hash")
@mix.reload
@ -50,15 +43,6 @@ describe Mix do
@mix.md5.should == "md5hash"
end
it "should re-run a mix if it was started a long time ago" do
this_mix = Mix.next("server")
@mix.reload
@mix.started_at -= 1000000
@mix.save
this_mix = Mix.next("server")
this_mix.id.should == @mix.id
end
it "signs url" do
stub_const("APP_CONFIG", app_config)
@mix.sign_url.should_not be_nil

View File

@ -1,30 +1,285 @@
require 'spec_helper'
require 'fileutils'
# these tests avoid the use of ActiveRecord and FactoryGirl to do blackbox, non test-instrumented tests
describe AudioMixer do
include UsesTempFiles
let(:audiomixer) { AudioMixer.new }
let(:manifest) { {} }
let(:valid_manifest) { make_manifest(manifest) }
def valid_files(manifest)
manifest["files"] = [ {"codec" => "vorbis", "offset" => 0, "filename" => "/some/path"} ]
end
def valid_output(manifest)
manifest["output"] = { "codec" => "vorbis" }
end
def valid_timeline(manifest)
manifest["timeline"] = []
end
def valid_recording(manifest)
manifest["recording_id"] = "record1"
end
def valid_mix(manifest)
manifest["mix_id"] = "mix1"
end
def make_manifest(manifest)
valid_files(manifest)
valid_output(manifest)
valid_timeline(manifest)
valid_recording(manifest)
valid_mix(manifest)
return manifest
end
before(:each) do
stub_const("APP_CONFIG", app_config)
end
describe "validate" do
it "no manifest" do
expect { audiomixer.validate }.to raise_error("no manifest specified")
end
it "no files specified" do
expect{ audiomixer.run({}) }.to raise_error("no files specified")
audiomixer.manifest = manifest
expect { audiomixer.validate }.to raise_error("no files specified")
end
it "no codec specified" do
expect{ audiomixer.run({ "files" => [ {"offset" => 0, "filename" => "/some/path"} ] }) }.to raise_error("no codec specified")
audiomixer.manifest = { "files" => [ {"offset" => 0, "filename" => "/some/path"} ] }
expect { audiomixer.validate }.to raise_error("no codec specified")
end
it "no offset specified" do
expect{ audiomixer.run({ "files" => [ {"codec" => "vorbis", "filename" => "/some/path"} ] }) }.to raise_error("no offset specified")
audiomixer.manifest = { "files" => [ {"codec" => "vorbis", "filename" => "/some/path"} ] }
expect { audiomixer.validate }.to raise_error("no offset specified")
end
it "no output specified" do
expect{ audiomixer.run({ "files" => [ {"codec" => "vorbis", "offset" => 0, "filename" => "/some/path"} ] }) }.to raise_error("no output specified")
valid_files(manifest)
audiomixer.manifest = manifest
audiomixer.manifest = { "files" => [ {"codec" => "vorbis", "offset" => 0, "filename" => "/some/path"} ] }
expect { audiomixer.validate }.to raise_error("no output specified")
end
it "no recording_id specified" do
valid_files(manifest)
valid_output(manifest)
valid_timeline(manifest)
valid_mix(manifest)
audiomixer.manifest = manifest
expect { audiomixer.validate }.to raise_error("no recording_id specified")
end
it "no mix_id specified" do
valid_files(manifest)
valid_output(manifest)
valid_timeline(manifest)
valid_recording(manifest)
audiomixer.manifest = manifest
expect { audiomixer.validate }.to raise_error("no mix_id specified")
end
end
describe "fetch_audio_files" do
it "get upset if file doesn't exist" do
audiomixer.manifest = { "files" => [ {"codec" => "vorbis", "offset" => 0, "filename" => "/some/bogus/path"} ] }
expect { audiomixer.fetch_audio_files }.to raise_error("no file located at: /some/bogus/path")
end
end
describe "prepare_manifest" do
it "writes manifest as json to file" do
audiomixer.manifest = valid_manifest
audiomixer.prepare_manifest
File.read(audiomixer.manifest_file).should == valid_manifest.to_json
end
end
describe "prepare_output" do
it "can be written to" do
audiomixer.manifest = valid_manifest
audiomixer.prepare_output
File.open(audiomixer.manifest[:output][:filename] ,"w") do |f|
f.write("tickle")
end
File.read(audiomixer.manifest[:output][:filename]).should == "tickle"
end
end
describe "prepare_error_out" do
it "can be written to" do
audiomixer.manifest = valid_manifest
audiomixer.prepare_error_out
File.open(audiomixer.manifest[:error_out] ,"w") do |f|
f.write("some_error")
end
File.read(audiomixer.manifest[:error_out]).should == "some_error"
end
end
describe "integration" do
sample_ogg='sample.ogg'
in_directory_with_file(sample_ogg)
before(:each) do
content_for_file("ogg goodness")
@user = FactoryGirl.create(:user)
@connection = FactoryGirl.create(:connection, :user => @user)
@instrument = FactoryGirl.create(:instrument, :description => 'a great instrument')
@track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument)
@music_session = FactoryGirl.create(:music_session, :creator => @user, :musician_access => true)
@music_session.connections << @connection
@music_session.save
@recording = Recording.start(@music_session, @user)
@recording.stop
@recording.claim(@user, "name", "description", Genre.first, true, true)
@recording.errors.any?.should be_false
end
describe "simulated" do
let (:local_files_manifest) {
{
:files => [ { :codec => :vorbis, :offset => 0, :filename => sample_ogg} ],
:output => { :codec => :vorbis },
:timeline => [ {:timestamp => 0, :mix => [{:balance => 0, :level => 100}]} ],
:recording_id => "recording1"
}
}
# stub out methods that are environmentally sensitive (so as to skip s3, and not run an actual audiomixer)
before(:each) do
AudioMixer.any_instance.stub(:execute) do |manifest_file|
output_filename = JSON.parse(File.read(manifest_file))['output']['filename']
FileUtils.touch output_filename
end
AudioMixer.any_instance.stub(:postback) # don't actually post resulting off file up
end
describe "perform" do
# this case does not talk to redis, does not run the real audiomixer, and does not actually talk with s3
# but it does talk to the database and verifies all the other logic
it "success" do
@mix = Mix.schedule(@recording, local_files_manifest)
AudioMixer.perform(@mix.id, @mix.sign_url(60 * 60 * 24))
@mix.reload
@mix.completed.should be_true
@mix.length.should == 0
@mix.md5.should == 'd41d8cd98f00b204e9800998ecf8427e' # that's the md5 of a touched file
end
it "errored" do
local_files_manifest[:files][0][:filename] = '/some/path/to/nowhere'
@mix = Mix.schedule(@recording, local_files_manifest)
expect{ AudioMixer.perform(@mix.id, @mix.sign_url(60 * 60 * 24)) }.to raise_error
@mix.reload
@mix.completed.should be_false
@mix.error_count.should == 1
@mix.error_reason.should == "unhandled-job-exception"
@mix.error_detail.should == "no file located at: /some/path/to/nowhere"
end
end
describe "with resque-spec" do
before(:each) do
ResqueSpec.reset!
end
it "should have been enqueued because mix got scheduled" do
@mix = Mix.schedule(@recording, local_files_manifest)
AudioMixer.should have_queue_size_of(1)
end
it "should actually run the job" do
with_resque do
@mix = Mix.schedule(@recording, local_files_manifest)
end
@mix.reload
@mix.completed.should be_true
@mix.length.should == 0
@mix.md5.should == 'd41d8cd98f00b204e9800998ecf8427e' # that's the md5 of a touched file, which is what the stubbed :execute does
end
end
end
# these tests try to run the job with minimal faking. Here's what we still fake:
# we don't run audiomixer. audiomixer is tested already
# we don't run redis and actual resque, because that's tested by resque/resque-spec
describe "full", :aws => true do
let (:s3_manifest) {
{
:files => [ { :codec => :vorbis, :offset => 0, :filename => @s3_sample } ],
:output => { :codec => :vorbis },
:timeline => [ {:timestamp => 0, :mix => [{:balance => 0, :level => 100}]} ],
:recording_id => @recording.id
}
}
before(:each) do
test_config = app_config
key = "audiomixer/#{sample_ogg}"
@s3_manager = S3Manager.new(test_config.aws_bucket, test_config.aws_access_key_id, test_config.aws_secret_access_key)
# put a file in s3
@s3_manager.upload(key, sample_ogg)
# create a signed url that the job will use to fetch it back down
@s3_sample = @s3_manager.sign_url(key, :secure => false)
AudioMixer.any_instance.stub(:execute) do |manifest_file|
output_filename = JSON.parse(File.read(manifest_file))['output']['filename']
FileUtils.touch output_filename
end
end
it "completes" do
with_resque do
@mix = Mix.schedule(@recording, s3_manifest)
end
@mix.reload
@mix.completed.should be_true
@mix.length.should == 0
@mix.md5.should == 'd41d8cd98f00b204e9800998ecf8427e' # that's the md5 of a touched file, which is what the stubbed :execute does
end
it "fails" do
with_resque do
s3_manifest[:files][0][:filename] = @s3_manager.url('audiomixer/bogus.ogg', :secure => false) # take off some of the trailing chars of the url
expect{ Mix.schedule(@recording, s3_manifest) }.to raise_error
end
@mix = Mix.order('id desc').limit(1).first()
@mix.reload
@mix.completed.should be_false
@mix.error_reason.should == "unable to download"
#ResqueFailedJobMailer::Mailer.deliveries.count.should == 1
end
end
end
end

View File

@ -3,6 +3,8 @@ require 'active_record'
require 'jam_db'
require 'spec_db'
require 'uses_temp_files'
require 'resque_spec'
require 'resque_failed_job_mailer'
# recreate test database and migrate it
SpecDb::recreate_database
@ -38,6 +40,17 @@ CarrierWave.configure do |config|
config.enable_processing = false
end
#Resque::Failure::Notifier.configure do |config|
# config.from = 'dummy@jamkazam.com' # from address
# config.to = 'dummy@jamkazam.com' # to address
# config.include_payload = true # enabled by default
#end
#Resque::Failure::Multiple.classes = [Resque::Failure::Redis, Resque::Failure::Notifier]
#Resque::Failure.backend = Resque::Failure::Multiple
#uncomment the following line to use spork with the debugger
#require 'spork/ext/ruby-debug'
@ -62,8 +75,8 @@ Spork.prefork do
config.filter_run :focus
# you can mark a test as slow so that developers won't commonly hit it, but build server will http://blog.davidchelimsky.net/2010/06/14/filtering-examples-in-rspec-2/
config.filter_run_excluding slow: true unless ENV['RUN_SLOW_TESTS'] == "1" || ENV['SLOW'] == "1" || ENV['ALL_TESTS'] == "1"
config.filter_run_excluding aws: true unless ENV['RUN_AWS_TESTS'] == "1" || ENV['AWS'] == "1" || ENV['ALL_TESTS'] == "1"
config.filter_run_excluding slow: true unless run_tests? :slow
config.filter_run_excluding aws: true unless run_tests? :aws
config.before(:suite) do
DatabaseCleaner.strategy = :truncation, {:except => %w[instruments genres] }

View File

@ -39,12 +39,21 @@ def app_config
return klass.new
end
def run_tests? type
type = type.to_s.capitalize
ENV["RUN_#{type}_TESTS"] == "1" || ENV[type] == "1" || ENV['ALL_TESTS'] == "1"
end
def wipe_s3_test_bucket
test_config = app_config
s3 = AWS::S3.new(:access_key_id => test_config.aws_access_key_id,
:secret_access_key => test_config.aws_secret_access_key)
test_bucket = s3.buckets[JAMKAZAM_TESTING_BUCKET]
if test_bucket.name == JAMKAZAM_TESTING_BUCKET
#test_bucket.clear!
# don't bother if the user isn't doing AWS tests
if run_tests? :aws
test_config = app_config
s3 = AWS::S3.new(:access_key_id => test_config.aws_access_key_id,
:secret_access_key => test_config.aws_secret_access_key)
test_bucket = s3.buckets[JAMKAZAM_TESTING_BUCKET]
if test_bucket.name == JAMKAZAM_TESTING_BUCKET
#test_bucket.clear!
end
end
end

View File

@ -27,7 +27,7 @@ gem 'bcrypt-ruby', '3.0.1'
gem 'faker', '1.0.1'
gem 'will_paginate', '3.0.3'
gem 'bootstrap-will_paginate', '0.0.6'
gem 'em-websocket', '>=0.4.0'
gem 'em-websocket', '>=0.4.0' #, :path => '/Users/seth/workspace/em-websocket'
gem 'uuidtools', '2.1.2'
gem 'ruby-protocol-buffers', '1.2.2'
gem 'pg', '0.15.1'
@ -59,6 +59,8 @@ gem 'geokit-rails'
gem 'postgres_ext'
gem 'haml-rails'
gem 'resque'
gem 'resque-retry'
gem 'resque-failed-job-mailer'
gem 'quiet_assets', :group => :development
@ -109,6 +111,7 @@ end
group :production do
gem 'unicorn'
gem 'newrelic_rpm'
gem 'god'
end
group :package do

View File

@ -2,6 +2,7 @@
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
require 'resque/tasks'
require File.expand_path('../config/application', __FILE__)
SampleApp::Application.load_tasks

View File

@ -39,6 +39,7 @@ class ApiMixesController < ApiController
end
def download
# XXX: needs to permission check
redirect_to @mix.sign_url
end

View File

@ -164,5 +164,6 @@ include JamRuby
config.ga_ua = 'UA-44184562-2' # google analytics
config.ga_suppress_admin = true
config.redis_host = "localhost:6379"
end
end

View File

@ -11,6 +11,7 @@ development:
host: localhost
pool: 5
timeout: 5000
prepared_statements: false
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
@ -23,6 +24,7 @@ test: &test
host: localhost
pool: 5
timeout: 5000
prepared_statements: false
production:
adapter: postgresql
@ -32,6 +34,7 @@ production:
host: localhost
pool: 5
timeout: 5000
prepared_statements: false
cucumber:
<<: *test

View File

@ -0,0 +1,54 @@
rails_env = ENV['RAILS_ENV'] || "production"
rails_root = ENV['RAILS_ROOT'] || "/data/github/current"
num_workers = rails_env == 'production' ? 5 : 2
num_workers.times do |num|
God.watch do |w|
w.dir = "#{rails_root}"
w.name = "resque-#{num}"
w.group = 'resque'
w.interval = 30.seconds
w.env = {"QUEUE"=>"critical,high,low", "RAILS_ENV"=>rails_env}
w.start = "/usr/bin/rake -f #{rails_root}/Rakefile environment resque:work"
w.uid = 'git'
w.gid = 'git'
# restart if memory gets too high
w.transition(:up, :restart) do |on|
on.condition(:memory_usage) do |c|
c.above = 350.megabytes
c.times = 2
end
end
# determine the state on startup
w.transition(:init, { true => :up, false => :start }) do |on|
on.condition(:process_running) do |c|
c.running = true
end
end
# determine when process has finished starting
w.transition([:start, :restart], :up) do |on|
on.condition(:process_running) do |c|
c.running = true
c.interval = 5.seconds
end
# failsafe
on.condition(:tries) do |c|
c.times = 5
c.transition = :start
c.interval = 5.seconds
end
end
# start if process is not running
w.transition(:up, :start) do |on|
on.condition(:process_running) do |c|
c.running = false
end
end
end
end

View File

@ -1,17 +1,22 @@
if Rails.env == "development" && Rails.application.config.bootstrap_dev_users
# create one user per employee, +1 for peter2 because he asked for it
User.create_dev_user("Seth", "Call", "seth@jamkazam.com", "jam123", "Austin", "TX", "US", nil, 'http://www.jamkazam.com/assets/avatars/avatar_seth.jpg')
User.create_dev_user("Brian", "Smith", "briansmith@jamkazam.com", "jam123", "Apex", "NC", "US", nil, 'http://www.jamkazam.com/assets/avatars/avatar_brian.jpg')
User.create_dev_user("Peter", "Walker", "peter@jamkazam.com", "jam123", "Austin", "TX", "US", nil, 'http://www.jamkazam.com/assets/avatars/avatar_peter.jpg')
User.create_dev_user("Peter", "Walker", "peter2@jamkazam.com", "jam123", "Austin", "TX", "US", nil, 'http://www.jamkazam.com/assets/avatars/avatar_peter.jpg')
User.create_dev_user("David", "Wilson", "david@jamkazam.com", "jam123", "Austin", "TX", "US", nil, 'http://www.jamkazam.com/assets/avatars/avatar_david.jpg')
User.create_dev_user("Jonathon", "Wilson", "jonathon@jamkazam.com", "jam123", "Bozeman", "MT", "US", [{:instrument_id => "keyboard", :proficiency_level => 4, :priority => 1}], 'http://www.jamkazam.com/assets/avatars/avatar_jonathon.jpg')
User.create_dev_user("Jonathan", "Kolyer", "jonathan@jamkazam.com", "jam123", "Austin", "TX", "US", nil, nil)
User.create_dev_user("Oswald", "Becca", "os@jamkazam.com", "jam123", "Austin", "TX", "US", nil, nil)
User.create_dev_user("Anthony", "Davis", "anthony@jamkazam.com", "jam123", "Austin", "TX", "US", nil, nil)
User.create_dev_user("George", "Currie", "george@jamkazam.com", "jam123", "Austin", "TX", "US", nil, nil)
User.create_dev_user("Chris", "Doughty", "chris@jamkazam.com", "jam123", "Austin", "TX", "US", nil, nil)
# if we've run once before, don't run again
unless User.find_by_email("seth@jamkazam.com")
# create one user per employee, +1 for peter2 because he asked for it
User.create_dev_user("Seth", "Call", "seth@jamkazam.com", "jam123", "Austin", "TX", "US", nil, 'http://www.jamkazam.com/assets/avatars/avatar_seth.jpg')
User.create_dev_user("Brian", "Smith", "briansmith@jamkazam.com", "jam123", "Apex", "NC", "US", nil, 'http://www.jamkazam.com/assets/avatars/avatar_brian.jpg')
User.create_dev_user("Peter", "Walker", "peter@jamkazam.com", "jam123", "Austin", "TX", "US", nil, 'http://www.jamkazam.com/assets/avatars/avatar_peter.jpg')
User.create_dev_user("Peter", "Walker", "peter2@jamkazam.com", "jam123", "Austin", "TX", "US", nil, 'http://www.jamkazam.com/assets/avatars/avatar_peter.jpg')
User.create_dev_user("David", "Wilson", "david@jamkazam.com", "jam123", "Austin", "TX", "US", nil, 'http://www.jamkazam.com/assets/avatars/avatar_david.jpg')
User.create_dev_user("Jonathon", "Wilson", "jonathon@jamkazam.com", "jam123", "Bozeman", "MT", "US", [{:instrument_id => "keyboard", :proficiency_level => 4, :priority => 1}], 'http://www.jamkazam.com/assets/avatars/avatar_jonathon.jpg')
User.create_dev_user("Jonathan", "Kolyer", "jonathan@jamkazam.com", "jam123", "Austin", "TX", "US", nil, nil)
User.create_dev_user("Oswald", "Becca", "os@jamkazam.com", "jam123", "Austin", "TX", "US", nil, nil)
User.create_dev_user("Anthony", "Davis", "anthony@jamkazam.com", "jam123", "Austin", "TX", "US", nil, nil)
User.create_dev_user("George", "Currie", "george@jamkazam.com", "jam123", "Austin", "TX", "US", nil, nil)
User.create_dev_user("Chris", "Doughty", "chris@jamkazam.com", "jam123", "Austin", "TX", "US", nil, nil)
User.create_dev_user("Daniel", "Weigh", "daniel@jamkazam.com", "jam123", "Austin", "TX", "US", nil, nil)
end
end

View File

@ -2,13 +2,14 @@ require 'amqp'
require 'jam_ruby'
require 'bugsnag'
# Creates a connection to RabbitMQ.
# Creates a connection to RabbitMQ
# On that single connection, a channel is created (which is a way to multiplex multiple queues/topics over the same TCP connection with rabbitmq)
# Then connections to the client_exchange and user_exchange are made, and put into the MQRouter static variables
# If this code completes (which implies that Rails can start to begin with, because this is in an initializer),
# then the Rails app itself is free to send messages over these exchanges
# TODO: reconnect logic if rabbitmq goes down...
# Also starts websocket-gateway
module JamWebEventMachine
def self.run_em
@ -33,6 +34,15 @@ module JamWebEventMachine
Rails.logger.debug "MQRouter.user_exchange = #{MQRouter.user_exchange}"
end
end
if Rails.application.config.websocket_gateway_enable
Thread.new { JamWebsockets::Server.new.run :port => Rails.application.config.websocket_gateway_port,
:emwebsocket_debug => Rails.application.config.websocket_gateway_internal_debug,
:connect_time_stale => Rails.application.config.websocket_gateway_connect_time_stale,
:connect_time_expire => Rails.application.config.websocket_gateway_connect_time_expire }
end
end
end
@ -72,4 +82,4 @@ module JamWebEventMachine
end
end
JamWebEventMachine.start
JamWebEventMachine.start unless $rails_rake_task

View File

@ -0,0 +1 @@
Resque.redis = Rails.application.config.redis_host

View File

@ -0,0 +1,6 @@
require 'resque_failed_job_mailer'
Resque::Failure::Notifier.configure do |config|
config.from = 'seth@jamkazam.com'
config.to = 'seth@jamkazam.com'
end

View File

@ -1,7 +1,7 @@
if Rails.application.config.websocket_gateway_enable
JamWebsockets::Server.new.run :port => Rails.application.config.websocket_gateway_port,
:emwebsocket_debug => Rails.application.config.websocket_gateway_internal_debug,
:connect_time_stale => Rails.application.config.websocket_gateway_connect_time_stale,
:connect_time_expire => Rails.application.config.websocket_gateway_connect_time_expire
# Thread.new { JamWebsockets::Server.new.run :port => Rails.application.config.websocket_gateway_port,
# :emwebsocket_debug => Rails.application.config.websocket_gateway_internal_debug,
# :connect_time_stale => Rails.application.config.websocket_gateway_connect_time_stale,
# :connect_time_expire => Rails.application.config.websocket_gateway_connect_time_expire }
end

3
web/config/resque.yml Normal file
View File

@ -0,0 +1,3 @@
development: localhost:6379
test: localhost:6379
production: localhost:6379

14
web/lib/tasks/start.rake Normal file
View File

@ -0,0 +1,14 @@
# this rake file is meant to hold shortcuts/helpers for starting onerous command line executions
# bundle exec rake audiomixer
task :audiomixer do
Rails.application.config.websocket_gateway_enable = false # prevent websocket gateway from starting
Rake::Task['environment'].invoke
ENV['QUEUE'] = 'audiomixer'
Rake::Task['resque:work'].invoke
end

View File

@ -0,0 +1,19 @@
#!/bin/bash -l
# default config values
PORT=3000
BUILD_NUMBER=`cat /var/lib/jam-web/BUILD_NUMBER`
CONFIG_FILE="/etc/jam-web/audiomixer-worker-upstart.conf"
if [ -e "$CONFIG_FILE" ]; then
. "$CONFIG_FILE"
fi
# I don't like doing this, but the next command (bundle exec) retouches/generates
# the gemfile. This unfortunately means the next debian update doesn't update this file.
# Ultimately this means an old Gemfile.lock is left behind for a new package,
# and bundle won't run because it thinks it has the wrong versions of gems
rm -f Gemfile.lock
RAILS_ENV=production BUILD_NUMBER=$BUILD_NUMBER QUEUE=audiomixer exec bundle exec rake environment resque:work

View File

@ -0,0 +1,7 @@
description "audiomixer-worker"
start on startup
start on runlevel [2345]
stop on runlevel [016]
exec start-stop-daemon --start --chdir /var/lib/jam-web --exec /var/lib/jam-web/script/package/audiomixer-worker-upstart-run.sh

View File

@ -18,3 +18,41 @@ mkdir -p /var/log/$NAME
chown -R $USER:$GROUP /var/lib/$NAME
chown -R $USER:$GROUP /etc/$NAME
chown -R $USER:$GROUP /var/log/$NAME
# do the same for audiomixer-worker
NAME="audiomxer-worker"
USER="$NAME"
GROUP="$NAME"
# copy upstart file
cp /var/lib/$NAME/script/package/$NAME.conf /etc/init/$NAME.conf
mkdir -p /var/lib/$NAME/log
mkdir -p /var/lib/$NAME/tmp
mkdir -p /etc/$NAME
mkdir -p /var/log/$NAME
chown -R $USER:$GROUP /var/lib/$NAME
chown -R $USER:$GROUP /etc/$NAME
chown -R $USER:$GROUP /var/log/$NAME
# do the same for icecast-worker
NAME="icecast-worker"
USER="$NAME"
GROUP="$NAME"
# copy upstart file
cp /var/lib/$NAME/script/package/$NAME.conf /etc/init/$NAME.conf
mkdir -p /var/lib/$NAME/log
mkdir -p /var/lib/$NAME/tmp
mkdir -p /etc/$NAME
mkdir -p /var/log/$NAME
chown -R $USER:$GROUP /var/lib/$NAME
chown -R $USER:$GROUP /etc/$NAME
chown -R $USER:$GROUP /var/log/$NAME

View File

@ -25,3 +25,54 @@ then
userdel $NAME
fi
NAME="audiomixer-worker"
set -e
if [ "$1" = "remove" ]
then
set +e
# stop the process, if any is found. we don't want this failing to cause an error, though.
sudo stop $NAME
set -e
if [ -f /etc/init/$NAME.conf ]; then
rm /etc/init/$NAME.conf
fi
fi
if [ "$1" = "purge" ]
then
if [ -d /var/lib/$NAME ]; then
rm -rf /var/lib/$NAME
fi
userdel $NAME
fi
NAME="icecast-worker"
set -e
if [ "$1" = "remove" ]
then
set +e
# stop the process, if any is found. we don't want this failing to cause an error, though.
sudo stop $NAME
set -e
if [ -f /etc/init/$NAME.conf ]; then
rm /etc/init/$NAME.conf
fi
fi
if [ "$1" = "purge" ]
then
if [ -d /var/lib/$NAME ]; then
rm -rf /var/lib/$NAME
fi
userdel $NAME
fi

View File

@ -1,9 +1,10 @@
#!/bin/sh
set -eu
NAME="jam-web"
set -eu
HOME="/var/lib/$NAME"
USER="$NAME"
GROUP="$NAME"
@ -32,5 +33,81 @@ then
"$USER" >/dev/null
fi
# NISno longer a possible problem; stop ignoring errors
# NIS no longer a possible problem; stop ignoring errors
set -e
# do the same for audiomixer-worker
NAME="audiomixer-worker"
set -eu
HOME="/var/lib/$NAME"
USER="$NAME"
GROUP="$NAME"
# if NIS is used, then errors can occur but be non-fatal
if which ypwhich >/dev/null 2>&1 && ypwhich >/dev/null 2>&1
then
set +e
fi
if ! getent group "$GROUP" >/dev/null
then
addgroup --system "$GROUP" >/dev/null
fi
# creating user if it isn't already there
if ! getent passwd "$USER" >/dev/null
then
adduser \
--system \
--home $HOME \
--shell /bin/false \
--disabled-login \
--ingroup "$GROUP" \
--gecos "$USER" \
"$USER" >/dev/null
fi
# NIS no longer a possible problem; stop ignoring errors
set -e
# do the same for icecast-worker
NAME="icecast-worker"
set -eu
HOME="/var/lib/$NAME"
USER="$NAME"
GROUP="$NAME"
# if NIS is used, then errors can occur but be non-fatal
if which ypwhich >/dev/null 2>&1 && ypwhich >/dev/null 2>&1
then
set +e
fi
if ! getent group "$GROUP" >/dev/null
then
addgroup --system "$GROUP" >/dev/null
fi
# creating user if it isn't already there
if ! getent passwd "$USER" >/dev/null
then
adduser \
--system \
--home $HOME \
--shell /bin/false \
--disabled-login \
--ingroup "$GROUP" \
--gecos "$USER" \
"$USER" >/dev/null
fi
# NIS no longer a possible problem; stop ignoring errors
set -e

View File

@ -38,6 +38,8 @@ gem 'bugsnag'
gem 'geokit-rails'
gem 'postgres_ext'
gem 'resque'
gem 'resque-retry'
gem 'resque-failed-job-mailer'
group :development do
gem 'pry'

View File

@ -40,7 +40,6 @@ module JamWebsockets
@log.info "cleaning up server"
@router.cleanup
end
end
end
@ -49,7 +48,7 @@ module JamWebsockets
end
def start_websocket_listener(listen_ip, port, emwebsocket_debug)
EventMachine::WebSocket.start(:host => listen_ip, :port => port, :debug => emwebsocket_debug) do |ws|
EventMachine::WebSocket.run(:host => listen_ip, :port => port, :debug => emwebsocket_debug) do |ws|
@log.info "new client #{ws}"
@router.new_client(ws)
end