13996: Update tests for cleaner config access
[arvados.git] / services / api / test / unit / crunch_dispatch_test.rb
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 require 'test_helper'
6 require 'crunch_dispatch'
7 require 'helpers/git_test_helper'
8
9 class CrunchDispatchTest < ActiveSupport::TestCase
10   include GitTestHelper
11
12   test 'choose cheaper nodes first' do
13     act_as_system_user do
14       # Replace test fixtures with a set suitable for testing dispatch
15       Node.destroy_all
16
17       # Idle nodes with different prices
18       [['compute1', 3.20, 32],
19        ['compute2', 1.60, 16],
20        ['compute3', 0.80, 8]].each do |hostname, price, cores|
21         Node.create!(hostname: hostname,
22                      info: {
23                        'slurm_state' => 'idle',
24                      },
25                      properties: {
26                        'cloud_node' => {
27                          'price' => price,
28                        },
29                        'total_cpu_cores' => cores,
30                        'total_ram_mb' => cores*1024,
31                        'total_scratch_mb' => cores*10000,
32                      })
33       end
34
35       # Node with no price information
36       Node.create!(hostname: 'compute4',
37                    info: {
38                      'slurm_state' => 'idle',
39                    },
40                    properties: {
41                      'total_cpu_cores' => 8,
42                      'total_ram_mb' => 8192,
43                      'total_scratch_mb' => 80000,
44                    })
45
46       # Cheap but busy node
47       Node.create!(hostname: 'compute5',
48                    info: {
49                      'slurm_state' => 'alloc',
50                    },
51                    properties: {
52                      'cloud_node' => {
53                        'price' => 0.10,
54                      },
55                      'total_cpu_cores' => 32,
56                      'total_ram_mb' => 32768,
57                      'total_scratch_mb' => 320000,
58                    })
59     end
60
61     dispatch = CrunchDispatch.new
62     [[1, 16384, ['compute2']],
63      [2, 16384, ['compute2', 'compute1']],
64      [2, 8000, ['compute4', 'compute3']],
65     ].each do |min_nodes, min_ram, expect_nodes|
66       job = Job.new(uuid: 'zzzzz-8i9sb-382lhiizavzhqlp',
67                     runtime_constraints: {
68                       'min_nodes' => min_nodes,
69                       'min_ram_mb_per_node' => min_ram,
70                     })
71       nodes = dispatch.nodes_available_for_job_now job
72       assert_equal expect_nodes, nodes
73     end
74   end
75
76   test 'respond to TERM' do
77     lockfile = Rails.root.join 'tmp', 'dispatch.lock'
78     ENV['CRUNCH_DISPATCH_LOCKFILE'] = lockfile.to_s
79     begin
80       pid = Process.fork do
81         begin
82           dispatch = CrunchDispatch.new
83           dispatch.stubs(:did_recently).returns true
84           dispatch.run []
85         ensure
86           Process.exit!
87         end
88       end
89       assert_with_timeout 5, "Dispatch did not lock #{lockfile}" do
90         !can_lock(lockfile)
91       end
92     ensure
93       Process.kill("TERM", pid)
94     end
95     assert_with_timeout 20, "Dispatch did not unlock #{lockfile}" do
96       can_lock(lockfile)
97     end
98   end
99
100   test 'override --cgroup-root with CRUNCH_CGROUP_ROOT' do
101     ENV['CRUNCH_CGROUP_ROOT'] = '/path/to/cgroup'
102     Rails.configuration.Containers.JobsAPI.CrunchJobWrapper = "none"
103     act_as_system_user do
104       j = Job.create(repository: 'active/foo',
105                      script: 'hash',
106                      script_version: '4fe459abe02d9b365932b8f5dc419439ab4e2577',
107                      script_parameters: {})
108       ok = false
109       Open3.expects(:popen3).at_least_once.with do |*args|
110         if args.index(j.uuid)
111           ok = ((i = args.index '--cgroup-root') and
112                 (args[i+1] == '/path/to/cgroup'))
113         end
114         true
115       end.raises(StandardError.new('all is well'))
116       dispatch = CrunchDispatch.new
117       dispatch.parse_argv ['--jobs']
118       dispatch.refresh_todo
119       dispatch.start_jobs
120       assert ok
121     end
122   end
123
124   def assert_with_timeout timeout, message
125     t = 0
126     while (t += 0.1) < timeout
127       if yield
128         return
129       end
130       sleep 0.1
131     end
132     assert false, message + " (waited #{timeout} seconds)"
133   end
134
135   def can_lock lockfile
136     lockfile.open(File::RDWR|File::CREAT, 0644) do |f|
137       return f.flock(File::LOCK_EX|File::LOCK_NB)
138     end
139   end
140
141   test 'rate limit of partial line segments' do
142     act_as_system_user do
143       Rails.configuration.Containers.Logging.LogPartialLineThrottlePeriod = 1
144
145       job = {}
146       job[:bytes_logged] = 0
147       job[:log_throttle_bytes_so_far] = 0
148       job[:log_throttle_lines_so_far] = 0
149       job[:log_throttle_bytes_skipped] = 0
150       job[:log_throttle_is_open] = true
151       job[:log_throttle_partial_line_last_at] = Time.new(0)
152       job[:log_throttle_first_partial_line] = true
153
154       dispatch = CrunchDispatch.new
155
156       line = "first log line"
157       limit = dispatch.rate_limit(job, line)
158       assert_equal true, limit
159       assert_equal "first log line", line
160       assert_equal 1, job[:log_throttle_lines_so_far]
161
162       # first partial line segment is skipped and counted towards skipped lines
163       now = Time.now.strftime('%Y-%m-%d-%H:%M:%S')
164       line = "#{now} localhost 100 0 stderr [...] this is first partial line segment [...]"
165       limit = dispatch.rate_limit(job, line)
166       assert_equal true, limit
167       assert_includes line, "Rate-limiting partial segments of long lines", line
168       assert_equal 2, job[:log_throttle_lines_so_far]
169
170       # next partial line segment within throttle interval is skipped but not counted towards skipped lines
171       line = "#{now} localhost 100 0 stderr [...] second partial line segment within the interval [...]"
172       limit = dispatch.rate_limit(job, line)
173       assert_equal false, limit
174       assert_equal 2, job[:log_throttle_lines_so_far]
175
176       # next partial line after interval is counted towards skipped lines
177       sleep(1)
178       line = "#{now} localhost 100 0 stderr [...] third partial line segment after the interval [...]"
179       limit = dispatch.rate_limit(job, line)
180       assert_equal false, limit
181       assert_equal 3, job[:log_throttle_lines_so_far]
182
183       # this is not a valid line segment
184       line = "#{now} localhost 100 0 stderr [...] does not end with [...] and is not a partial segment"
185       limit = dispatch.rate_limit(job, line)
186       assert_equal true, limit
187       assert_equal "#{now} localhost 100 0 stderr [...] does not end with [...] and is not a partial segment", line
188       assert_equal 4, job[:log_throttle_lines_so_far]
189
190       # this also is not a valid line segment
191       line = "#{now} localhost 100 0 stderr does not start correctly but ends with [...]"
192       limit = dispatch.rate_limit(job, line)
193       assert_equal true, limit
194       assert_equal "#{now} localhost 100 0 stderr does not start correctly but ends with [...]", line
195       assert_equal 5, job[:log_throttle_lines_so_far]
196     end
197   end
198
199   test 'scancel orphaned job nodes' do
200     Rails.configuration.Containers.JobsAPI.CrunchJobWrapper = "slurm_immediate"
201     act_as_system_user do
202       dispatch = CrunchDispatch.new
203
204       squeue_resp = IO.popen("echo zzzzz-8i9sb-pshmckwoma9plh7\necho thisisnotvalidjobuuid\necho zzzzz-8i9sb-4cf0abc123e809j\necho zzzzz-dz642-o04e3r651turtdr\n")
205       scancel_resp = IO.popen("true")
206
207       IO.expects(:popen).
208         with(['squeue', '-a', '-h', '-o', '%j']).
209         returns(squeue_resp)
210
211       IO.expects(:popen).
212         with(dispatch.sudo_preface + ['scancel', '-n', 'zzzzz-8i9sb-4cf0abc123e809j']).
213         returns(scancel_resp)
214
215       dispatch.check_orphaned_slurm_jobs
216     end
217   end
218 end