afterparty 0.0.4 → 0.0.21

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
- metadata.gz: 31d453b1b76a1ae8297cbe7c139798a7351cd279
4
- data.tar.gz: 284c96ff3b0d214b8b28148be021e513cfe3126b
3
+ metadata.gz: 1746d9cd2e5dee4f459c02cf8a0bef335de1a05d
4
+ data.tar.gz: 86b2eb38f0c9113b8515fbe18ad8d3635a9dc0e9
5
5
  !binary "U0hBNTEy":
6
- metadata.gz: 1d0b960020c84b6b6e977f234c831b5599c6accecea974ad685309c1093ea9f86d44f79d14a7167a75aef6ab5d5f4615b9e2ba556efa8ba22abec453c74ee05f
7
- data.tar.gz: 1a528ba181430aa129c2507e401ab8da33305e1a4020751d4cdd475b5485491c246c3444a81c329c4c850fbeba0addba9167c7a4e11a2b7534353452e0590646
6
+ metadata.gz: 431465cc60c4fb3abbfc63f8f4294367baa94507c4ce04b9181d3afbd5ed4546d3b3d9a79a0178a37814fcbfe468def7fd8c36633baf47d7d55ce384c6929828
7
+ data.tar.gz: a8bcde980355c3a56a668450e274452d0ef10c0274e15bcc609a1a8ae3eb9d596c854ff60616e8b9e0648388c7f19974fdcfc6bb64dd8d806f0e37424355f093
@@ -3,6 +3,4 @@ rvm:
3
3
  - 1.9.3
4
4
  - 2.0.0
5
5
  services:
6
- - redis-server
7
- env:
8
- - AFTERPARTY_JOB_TIME=15 AFTERPARTY_SLOW_TIME=50
6
+ - redis-server
data/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # Afterparty
2
2
 
3
- [![Build Status](https://travis-ci.org/hstove/afterparty.png?branch=master)](https://travis-ci.org/hstove/afterparty)
4
-
5
- A Rails 4 compatible queue with support for executing jobs in the future and persistence with Redis.
3
+ A Rails 4 compatible queue with support for executing jobs in the future and serialization with Redis.
6
4
 
7
5
  ## Installation
8
6
 
@@ -43,7 +41,7 @@ end
43
41
  Then add it to the queue at any time.
44
42
 
45
43
  ~~~Ruby
46
- Rails.configuration.queue << Job.new
44
+ Rails.queue << Job.new
47
45
  ~~~
48
46
 
49
47
  If your job responds to an `execute_at` method, the queue will wait to process that job until the specified time.
@@ -19,7 +19,6 @@ Gem::Specification.new do |spec|
19
19
  spec.require_paths = ["lib"]
20
20
 
21
21
  spec.add_dependency "redis"
22
- spec.add_dependency "iconv"
23
22
 
24
23
  spec.add_development_dependency "bundler", "~> 1.3"
25
24
  spec.add_development_dependency "rake"
@@ -14,50 +14,4 @@ module Afterparty
14
14
  def self.redis=(redis)
15
15
  @@redis = redis
16
16
  end
17
-
18
- def self.clear namespace=:default
19
- redis_call namespace, :del
20
- end
21
-
22
- def self.redis_call namespace, command, *args
23
- @@redis.send(command, redis_queue_name(namespace), *args)
24
- end
25
-
26
- def self.redis_queue_name namespace=:default
27
- "afterparty_#{namespace}_queue"
28
- end
29
-
30
- def self.queues
31
- @@redis.smembers "afterparty_queues"
32
- end
33
-
34
- def self.add_queue name
35
- @@redis.sadd "afterparty_queues", name
36
- end
37
-
38
- def self.next_job_id namespace=:default
39
- @@redis.incr "afterparty_#{namespace.to_s}_job_id"
40
- end
41
-
42
- def self.load(raw)
43
- begin
44
- begin
45
- job = Marshal.load(raw)
46
- job = Marshal.load(job) if String === job
47
- return job
48
- rescue NameError => e
49
- # lots of marshal load errors are because something that hasn't been
50
- # required. recursively require on these errors
51
- name = e.message.gsub("uninitialized constant ","").downcase
52
- begin
53
- require "#{name}"
54
- return load(raw)
55
- rescue LoadError
56
- end
57
- end
58
- rescue
59
- return nil
60
- end
61
- end
62
-
63
17
  end
@@ -4,9 +4,8 @@ module Afterparty
4
4
  @temp_namespace = namespace
5
5
  end
6
6
 
7
- def redis_queue_name
8
- puts (a = Afterparty.redis_queue_name(@temp_namespace || @options[:namespace]))
9
- a
7
+ def redis_queue_name
8
+ "afterparty_#{@temp_namespace || @options[:namespace]}_queue"
10
9
  end
11
10
 
12
11
  def clear
@@ -14,7 +13,7 @@ module Afterparty
14
13
  end
15
14
 
16
15
  def redis_call command, *args
17
- result = Afterparty.redis_call (@temp_namespace || @options[:namespace]), command, *args
16
+ result = Afterparty.redis.send(command, redis_queue_name, *args)
18
17
  @temp_namespace = nil
19
18
  result
20
19
  end
@@ -32,17 +31,13 @@ module Afterparty
32
31
  end
33
32
 
34
33
  def jobs_with_scores
35
- hash_from_scores(redis_call(:zrange, 0, -1, {withscores: true}))
34
+ redis_call :zrange, 0, -1, {withscores: true}
36
35
  end
37
36
 
38
37
  def valid_jobs
39
38
  redis_call :zrangebyscore, 0, Time.now.to_i
40
39
  end
41
40
 
42
- def next_valid_job
43
- valid_jobs.first
44
- end
45
-
46
41
  def jobs_empty?
47
42
  count = total_jobs_count
48
43
  # ap count
@@ -57,32 +52,8 @@ module Afterparty
57
52
  @@redis
58
53
  end
59
54
 
60
- def last_completed
61
- @temp_namespace = "completed"
62
- redis_call(:zrange, -1, -1).first
63
- end
64
-
65
- def completed
66
- @temp_namespace = "completed"
67
- redis_call(:zrange, -20, -1).reverse
68
- end
69
-
70
- def completed_with_scores
71
- @temp_namespace = "completed"
72
- hash_from_scores(redis_call(:zrange, -20, -1, withscores: true)).reverse
73
- end
74
-
75
-
76
55
  private
77
56
 
78
- def hash_from_scores raw
79
- arr = []
80
- raw.each do |group|
81
- arr << Afterparty::JobContainer.new(group[0], group[1])
82
- end
83
- arr
84
- end
85
-
86
57
  # returns true if job has an :execute_at value
87
58
  def job_valid? job
88
59
  job.respond_to?(:execute_at) && !job.execute_at.nil?
@@ -4,10 +4,9 @@ module Afterparty
4
4
  include Afterparty::QueueHelpers
5
5
 
6
6
  def initialize options={}, consumer_options={}
7
- # @consumer = ThreadedQueueConsumer.new(self, consumer_options).start
7
+ @consumer = ThreadedQueueConsumer.new(self, consumer_options).start
8
8
  @options = options
9
9
  @options[:namespace] ||= "default"
10
- Afterparty.add_queue @options[:namespace]
11
10
  @options[:sleep] ||= 5
12
11
  @mutex = Mutex.new
13
12
  end
@@ -15,13 +14,8 @@ module Afterparty
15
14
  def push job
16
15
  @mutex.synchronize do
17
16
  return nil if job.nil?
18
- job.class.module_eval do
19
- attr_accessor :afterparty_job_id, :afterparty_queue
20
- end
21
- queue_name = @temp_namespace || @options[:namespace]
22
- job.afterparty_queue = queue_name
23
- job.afterparty_job_id = Afterparty.next_job_id queue_name
24
17
  async_redis_call{ redis_call :zadd, queue_time(job), Marshal.dump(job) }
18
+ @consumer.start unless @consumer.thread.alive?
25
19
  @temp_namespace = nil
26
20
  end
27
21
  end
@@ -31,20 +25,16 @@ module Afterparty
31
25
  def pop
32
26
  @mutex.synchronize do
33
27
  while true do
34
- if !(_jobs = valid_jobs).empty?
28
+ if jobs_empty?
29
+ @consumer.shutdown
30
+ elsif !(_jobs = valid_jobs).empty?
35
31
  job_dump = _jobs[0]
36
32
  async_redis_call do
37
33
  redis_call :zrem, job_dump
38
34
  @temp_namespace = "completed"
39
35
  redis_call :zadd, Time.now.to_i, job_dump
40
36
  end
41
- begin
42
- return Marshal.load(job_dump)
43
- rescue ArgumentException => e
44
- puts "You encountered an argument exception while deserializing a job."
45
- puts "Message: #{e.message}"
46
- raise e
47
- end
37
+ return Marshal.load(job_dump)
48
38
  end
49
39
  sleep(@options[:sleep])
50
40
  end
@@ -1,3 +1,3 @@
1
1
  module Afterparty
2
- VERSION = "0.0.4"
2
+ VERSION = "0.0.21"
3
3
  end
@@ -8,16 +8,11 @@ describe Afterparty::RedisQueue do
8
8
  @q = Afterparty::TestRedisQueue.new({sleep: 0.5})
9
9
  end
10
10
 
11
- after do
12
- @worker.stop
13
- end
14
-
15
11
  before :each do
16
- @worker = Afterparty::Worker.new({sleep: 0.5})
17
- @worker.consume
12
+ @q.completed_jobs.clear
18
13
  @q.clear
19
- @job_time = (ENV['AFTERPARTY_JOB_TIME'] || 3).to_i
20
- @slow_job_time = (ENV['AFTERPARTY_SLOW_TIME'] || 10).to_i
14
+ Afterparty.redis.quit
15
+ @job_time = 10
21
16
  end
22
17
 
23
18
  it "pushes nil without errors" do
@@ -33,15 +28,15 @@ describe Afterparty::RedisQueue do
33
28
  it "executes the job" do
34
29
  job = TestJob.new
35
30
  @q.push(job)
36
- @q.jobs.size.should eq(1)
31
+ complete.size.should eq(0)
37
32
  chill(@job_time)
38
- @q.jobs.size.should eq(0)
33
+ complete.size.should eq(1)
39
34
  end
40
35
 
41
36
  it "removes items from the queue after running them" do
42
37
  @q.push TestJob.new
43
38
  chill(@job_time)
44
- @q.jobs.size.should == 0
39
+ @q.jobs.should_not include(@job)
45
40
  end
46
41
 
47
42
  it "doesn't execute jobs that execute in a while" do
@@ -49,19 +44,18 @@ describe Afterparty::RedisQueue do
49
44
  job.execute_at = Time.now + 200
50
45
  @q.push job
51
46
  chill(@job_time)
52
- @q.jobs.size.should eq(1)
47
+ complete.size.should eq(0)
53
48
  end
54
49
 
55
50
  it "waits the correct amount of time to execute a job" do
56
51
  job = TestJob.new
57
- job.execute_at = Time.now + 5
52
+ job.execute_at = Time.now + 2
58
53
  @q.push(job)
59
- @q.jobs.size.should eq(1)
60
- chill(@slow_job_time)
61
- @q.jobs.size.should eq(0)
54
+ chill(50)
55
+ complete.size.should eq(1)
62
56
  end
63
57
 
64
- it "doesn't wait and execute the job synchronously when added" do
58
+ it "doesn't execute the job synchronously when added" do
65
59
  job = test_job 100
66
60
  t = Time.now
67
61
  @q.push(job)
@@ -74,8 +68,8 @@ describe Afterparty::RedisQueue do
74
68
  @q.push(late_job)
75
69
  @q.push(early_job)
76
70
  chill(@job_time)
77
- (jobs = @q.jobs).size.should eq(1)
78
- jobs[0].execute_at.should_not be(nil)
71
+ complete.size.should eq(1)
72
+ complete[0].execute_at.should be(nil)
79
73
  end
80
74
 
81
75
  class ErrorJob
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: afterparty
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.21
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hank Stoever
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-05-17 00:00:00.000000000 Z
11
+ date: 2013-05-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -24,20 +24,6 @@ dependencies:
24
24
  - - ! '>='
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
- - !ruby/object:Gem::Dependency
28
- name: iconv
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ! '>='
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ! '>='
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
27
  - !ruby/object:Gem::Dependency
42
28
  name: bundler
43
29
  requirement: !ruby/object:Gem::Requirement
@@ -82,22 +68,12 @@ files:
82
68
  - README.md
83
69
  - Rakefile
84
70
  - afterparty.gemspec
85
- - app/assets/javascripts/afterparty.js.coffee
86
- - app/assets/stylesheets/afterparty.css.sass
87
- - app/controllers/afterparty/dashboard_controller.rb
88
- - app/views/afterparty/dashboard/index.html.haml
89
- - config/routes.rb
90
71
  - dump.rdb
91
72
  - lib/afterparty.rb
92
- - lib/afterparty/engine.rb
93
- - lib/afterparty/job_container.rb
94
- - lib/afterparty/jobs.rb
95
73
  - lib/afterparty/queue_helpers.rb
96
74
  - lib/afterparty/redis_queue.rb
97
75
  - lib/afterparty/threaded_queue_consumer.rb
98
76
  - lib/afterparty/version.rb
99
- - lib/afterparty/worker.rb
100
- - lib/tasks/tasks.rake
101
77
  - spec/helpers.rb
102
78
  - spec/redis_queue_spec.rb
103
79
  - spec/spec_helper.rb
@@ -1,14 +0,0 @@
1
- $(document).ready ->
2
- # $('.debug').click (e) ->
3
- # $el = $(e.target)
4
- # $tr = $el.parents('tr')
5
- # $tr.next().toggle()
6
- # $tr.toggleClass('debugged')
7
- # false
8
- $('.job-row').click (e) ->
9
- $el = $(e.target)
10
- return true if $el.hasClass('job-action')
11
- $tr = $el.parents('tr')
12
- $tr.next().toggle()
13
- $tr.toggleClass('debugged')
14
- false
@@ -1,85 +0,0 @@
1
- body
2
- :font-family helvetica
3
- :background-color whitesmoke
4
- :color lighten(black,30%)
5
- :margin 0 20px
6
-
7
- a:visited, a
8
- :color rgb(96, 89, 180)
9
- :text-decoration none
10
-
11
- span
12
- :font-size 12px
13
-
14
- .job-table
15
- :border-spacing 0
16
- :max-width 100%
17
- :margin 0px auto
18
- thead tr:first-child
19
- th
20
- :border-width 1px 1px 1px 0
21
- &:first-child
22
- :border-width 1px 1px 1px 1px
23
- th:first-child
24
- :border-top-left-radius 2px
25
- th:last-child
26
- :border-top-right-radius 2px
27
- tbody tr:last-child
28
- td:first-child
29
- :border-bottom-left-radius 2px
30
- td:last-child
31
- :border-bottom-right-radius 2px
32
- tr
33
- td, th
34
- :border 1px solid #ccc
35
- :border-width 0 1px 1px 0
36
- :background-color lighten(whitesmoke, 5%)
37
- :padding 12px 5px
38
- :text-align left
39
- :vertical-align middle
40
- :word-wrap break-word
41
- &:nth-child(2)
42
- :width 70%
43
- &:first-child
44
- :border-width 0 1px 1px 1px
45
- &:first-child, &:last-child
46
- :width 15% !important
47
- &.debugged
48
- td
49
- :background-color rgba(255,255,0,0.25)
50
- &.job-row
51
- :cursor pointer
52
- h3
53
- :margin 0
54
- &:hover
55
- td
56
- :background-color rgba(0,0,0,0.15)
57
- .description
58
- :color white
59
- .debug-row
60
- :display none
61
- :max-width 100%
62
- td pre
63
- :word-wrap break-word
64
- :white-space pre-wrap
65
-
66
- .distance-past, .distance-future
67
- :font-size 12px
68
- .distance-future
69
- :color darken(rgb(0,255,0), 20%)
70
- .distance-past
71
- :color rgba(255,0,0,0.6)
72
-
73
- .description
74
- :font-size 12px
75
- :color #bbb
76
- :margin-left 10px
77
-
78
- .job-action
79
- :text-decoration underline
80
- :font-size 12px
81
- :margin 0 2px
82
-
83
- .job_id
84
- :color #bbb
85
- :font-weight 700
@@ -1,20 +0,0 @@
1
- module Afterparty
2
- class DashboardController < ApplicationController
3
- layout false
4
- def index
5
- @queues = Afterparty.queues
6
- if params[:completed]
7
- @jobs = queue.completed_with_scores
8
- else
9
- @jobs = queue.jobs_with_scores
10
- end
11
- end
12
-
13
- def queue
14
- Rails.configuration.queue
15
- end
16
-
17
- def run
18
- end
19
- end
20
- end
@@ -1,67 +0,0 @@
1
- !!!
2
- %html
3
-
4
- %head
5
- %title== #{Rails.application.class.parent_name} Job Queue
6
- %meta{"http-equiv"=>"Content-Type", :content=>"text/html; charset=utf-8"}
7
- / %meta{name: "viewport", content: "width=device-width, initial-scale=1.0"}
8
- = stylesheet_link_tag "afterparty"
9
- = javascript_include_tag "jquery", "afterparty"
10
- = csrf_meta_tag
11
- = favicon_link_tag
12
- = yield(:head)
13
-
14
- %body
15
- %h1
16
- Viewing
17
- - if params[:completed]
18
- = pluralize @jobs.size, "completed job"
19
- %h3= link_to "View Job Queue", afterparty_engine.dashboard_path
20
- - else
21
- = pluralize @jobs.size, "job"
22
- %h3= link_to "View Completed Jobs", afterparty_engine.dashboard_path(completed: true)
23
- = @queues
24
- %table.job-table
25
- %thead
26
- %tr
27
- %th Execute At
28
- / %th Job
29
- / %th Actions
30
- %tbody
31
- - if @jobs.empty?
32
- %tr
33
- %td{colspan: 3}
34
- %em No jobs to show...
35
- - else
36
- - @jobs.each do |job_container|
37
- - job = job_container.job
38
- %tr.job-row
39
- %td
40
- %h3
41
- - if job_container.job
42
- = link_to job_container.job_class, "#", class: 'debug'
43
- - if job_container.job.respond_to? :description
44
- %span.description= job_container.job.description.html_safe
45
- - else
46
- %em Error marshaling job
47
- =# job_container.execute_at.strftime("%B %d, %Y at %l:%M %P")
48
- - distance = time_ago_in_words(job_container.execute_at)
49
- - if job_container.execute_at > Time.now
50
- %span.distance-future= "in #{distance}"
51
- - else
52
- %span.distance-past= "#{distance} ago"
53
- - if (_id = job_container.job_id) && (_queue = job_container.queue_name)
54
- %span.job_id== ##{_id} #{_queue}
55
- - unless params[:completed]
56
- = link_to "run", afterparty_engine.run_job_path(job_id: _id, queue: _queue), class: 'job-action'
57
- = link_to "delete", afterparty_engine.delete_job_path(job_id: _id, queue: _queue), class: 'job-action'
58
- %tr.debug-row
59
- %td
60
- - if job_container.job
61
- = debug job_container.job
62
- - else
63
- = job_container.raw_string
64
-
65
- %p
66
- Current Time:
67
- = Time.now.strftime("%B %d, %Y at %l:%M %P")
@@ -1,9 +0,0 @@
1
- Afterparty::Engine.routes.draw do
2
- get "/" => "afterparty/dashboard#index", as: :dashboard
3
- get "/run" => "afterparty/dashboard#run", as: :run_job
4
- get "/delete" => "afterparty/dashboard#run", as: :delete_job
5
- end
6
-
7
- Rails.application.routes.draw do
8
- mount Afterparty::Engine, at: "afterparty", as: "afterparty_engine"
9
- end
@@ -1,7 +0,0 @@
1
- require 'rails'
2
- module Afterparty
3
- class Engine < Rails::Engine
4
- engine_name :afterparty
5
-
6
- end
7
- end
@@ -1,36 +0,0 @@
1
- require 'iconv'
2
- require 'date'
3
-
4
- module Afterparty
5
- class JobContainer
6
- attr_accessor :job, :raw, :execute_at, :job_id, :queue_name
7
-
8
- #intialized from redis's WITHSCORES function
9
- def initialize _raw, timestamp
10
- @execute_at = Time.at(timestamp)
11
- begin
12
- @job = Afterparty.load(_raw)
13
- @job_id = job.afterparty_job_id if @job.respond_to? :afterparty_job_id
14
- @queue_name = job.afterparty_queue if @job.respond_to? :afterparty_queue
15
- rescue Exception => e
16
- ap "Error during load: #{e.message}"
17
- @job = nil
18
- end
19
- @raw = _raw
20
- self
21
- end
22
-
23
- def job_class
24
- if @job
25
- @job.class
26
- else
27
- nil
28
- end
29
- end
30
-
31
- def raw_string
32
- ic = Iconv.new('UTF-8//IGNORE', 'UTF-8')
33
- ic.iconv(@raw.dup + ' ')[0..-2]
34
- end
35
- end
36
- end
@@ -1,22 +0,0 @@
1
- module Afterparty
2
- class MailerJob
3
- attr_accessor :execute_at, :mail, :clazz, :method, :args
4
- def initialize clazz, method, *args
5
- # @mail = UserMailer.welcome_email(User.find(1))
6
- @clazz = UserMailer
7
- @method = method
8
- @args = args
9
- end
10
-
11
- def run
12
- @mail = @clazz.send @method, *@args
13
- @mail.deliver
14
- end
15
-
16
- def description
17
- desc = "Mailer: #{(@clazz || "nil")}."
18
- desc << "Method: #{(@method || nil)}."
19
- desc << "Args: #{(@args || nil)}"
20
- end
21
- end
22
- end
@@ -1,56 +0,0 @@
1
- module Afterparty
2
- class Worker
3
- include QueueHelpers
4
-
5
- def initialize options = {}
6
- @options = options
7
- @options[:adapter] ||= :redis
8
- @options[:namespace] ||= :default
9
- @options[:sleep] ||= 10
10
- @options[:logger] ||= Logger.new($stderr)
11
- self
12
- end
13
-
14
- def consume
15
- @stopped = false
16
- # puts "starting worker with namespace [#{@options[:namespace]}]."
17
- @thread = Thread.new {
18
- consume_sync
19
- }
20
- @thread
21
- end
22
-
23
- def consume_sync
24
- while !@stopped
25
- job = next_valid_job
26
- if job
27
- async_redis_call do
28
- @temp_namespace = "completed"
29
- redis_call :zadd, Time.now.to_i, Marshal.dump(job)
30
- redis_call :zrem, job
31
- end
32
- run job
33
- else
34
- sleep(@options[:sleep])
35
- end
36
- end
37
- end
38
-
39
- def stop
40
- @stopped = true
41
- @thread.join(0)
42
- end
43
-
44
- def run(job)
45
- fork do
46
- Marshal.load(job).run
47
- end
48
- rescue Exception => exception
49
- handle_exception job, exception
50
- end
51
-
52
- def handle_exception(job, exception)
53
- @options[:logger].error "Job Error: #{job.inspect}\n#{exception.message}\n#{exception.backtrace.join("\n")}"
54
- end
55
- end
56
- end
@@ -1,25 +0,0 @@
1
- namespace :jobs do
2
- require 'mail'
3
-
4
- desc "Start a new worker"
5
- task work: :environment do
6
- worker = Afterparty::Worker.new
7
- worker.consume_sync
8
- end
9
-
10
- desc "Clear all jobs"
11
- task clear: :environment do
12
- Rails.configuration.queue.clear
13
- end
14
-
15
- desc "List Jobs"
16
- task list: :environment do
17
- jobs = Rails.configuration.queue.jobs_with_scores
18
- puts "#{jobs.values.size} total jobs."
19
- jobs.each do |time, job|
20
- puts time
21
- puts job
22
- end
23
- end
24
- end
25
-