17074: Refactor and simplify favorites middleware.
[arvados.git] / services / api / test / unit / container_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 'helpers/container_test_helper'
7
8 class ContainerTest < ActiveSupport::TestCase
9   include DbCurrentTime
10   include ContainerTestHelper
11
12   DEFAULT_ATTRS = {
13     command: ['echo', 'foo'],
14     container_image: 'fa3c1a9cb6783f85f2ecda037e07b8c3+167',
15     output_path: '/tmp',
16     priority: 1,
17     runtime_constraints: {"vcpus" => 1, "ram" => 1, "cuda" => {"device_count":0, "driver_version": "", "hardware_capability": ""}},
18   }
19
20   REUSABLE_COMMON_ATTRS = {
21     container_image: "9ae44d5792468c58bcf85ce7353c7027+124",
22     cwd: "test",
23     command: ["echo", "hello"],
24     output_path: "test",
25     output_glob: [],
26     runtime_constraints: {
27       "API" => false,
28       "keep_cache_disk" => 0,
29       "keep_cache_ram" => 0,
30       "ram" => 12000000000,
31       "vcpus" => 4
32     },
33     mounts: {
34       "test" => {"kind" => "json"},
35     },
36     environment: {
37       "var" => "val",
38     },
39     secret_mounts: {},
40     runtime_user_uuid: "zzzzz-tpzed-xurymjxw79nv3jz",
41     runtime_auth_scopes: ["all"],
42     scheduling_parameters: {},
43   }
44
45   REUSABLE_ATTRS_SLIM = {
46     command: ["echo", "slim"],
47     container_image: "9ae44d5792468c58bcf85ce7353c7027+124",
48     cwd: "test",
49     environment: {},
50     mounts: {},
51     output_path: "test",
52     output_glob: [],
53     runtime_auth_scopes: ["all"],
54     runtime_constraints: {
55       "API" => false,
56       "keep_cache_disk" => 0,
57       "keep_cache_ram" => 0,
58       "ram" => 8 << 30,
59       "vcpus" => 4
60     },
61     runtime_user_uuid: "zzzzz-tpzed-xurymjxw79nv3jz",
62     secret_mounts: {},
63     scheduling_parameters: {},
64   }
65
66   def request_only attrs
67     attrs.reject {|k| [:runtime_user_uuid, :runtime_auth_scopes].include? k}
68   end
69
70   def minimal_new attrs={}
71     cr = ContainerRequest.new request_only(DEFAULT_ATTRS.merge(attrs))
72     cr.state = ContainerRequest::Committed
73     cr.save!
74     c = Container.find_by_uuid cr.container_uuid
75     assert_not_nil c
76     return c, cr
77   end
78
79   def check_illegal_updates c, bad_updates
80     bad_updates.each do |u|
81       refute c.update(u), u.inspect
82       refute c.valid?, u.inspect
83       c.reload
84     end
85   end
86
87   def check_illegal_modify c
88     check_illegal_updates c, [{command: ["echo", "bar"]},
89                               {container_image: "arvados/apitestfixture:june10"},
90                               {cwd: "/tmp2"},
91                               {environment: {"FOO" => "BAR"}},
92                               {mounts: {"FOO" => "BAR"}},
93                               {output_path: "/tmp3"},
94                               {locked_by_uuid: api_client_authorizations(:admin).uuid},
95                               {auth_uuid: api_client_authorizations(:system_user).uuid},
96                               {runtime_constraints: {"FOO" => "BAR"}}]
97   end
98
99   def check_bogus_states c
100     check_illegal_updates c, [{state: nil},
101                               {state: "Flubber"}]
102   end
103
104   def check_no_change_from_cancelled c
105     check_illegal_modify c
106     check_bogus_states c
107     check_illegal_updates c, [{ priority: 3 },
108                               { state: Container::Queued },
109                               { state: Container::Locked },
110                               { state: Container::Running },
111                               { state: Container::Complete }]
112   end
113
114   test "Container create" do
115     act_as_system_user do
116       c, _ = minimal_new(environment: {},
117                       mounts: {"BAR" => {"kind" => "FOO"}},
118                       output_path: "/tmp",
119                       priority: 1,
120                       runtime_constraints: {"vcpus" => 1, "ram" => 1})
121
122       check_illegal_modify c
123       check_bogus_states c
124
125       c.reload
126       c.priority = 2
127       c.save!
128     end
129   end
130
131   test "Container valid priority" do
132     act_as_system_user do
133       c, _ = minimal_new(environment: {},
134                       mounts: {"BAR" => {"kind" => "FOO"}},
135                       output_path: "/tmp",
136                       priority: 1,
137                       runtime_constraints: {"vcpus" => 1, "ram" => 1})
138
139       assert_raises(ActiveRecord::RecordInvalid) do
140         c.priority = -1
141         c.save!
142       end
143
144       c.priority = 0
145       c.save!
146
147       c.priority = 1
148       c.save!
149
150       c.priority = 500
151       c.save!
152
153       c.priority = 999
154       c.save!
155
156       c.priority = 1000
157       c.save!
158
159       c.priority = 1000 << 50
160       c.save!
161     end
162   end
163
164   test "Container runtime_status data types" do
165     set_user_from_auth :active
166     attrs = {
167       environment: {},
168       mounts: {"BAR" => {"kind" => "FOO"}},
169       output_path: "/tmp",
170       priority: 1,
171       runtime_constraints: {"vcpus" => 1, "ram" => 1}
172     }
173     c, _ = minimal_new(attrs)
174     assert_equal c.runtime_status, {}
175     assert_equal Container::Queued, c.state
176
177     set_user_from_auth :system_user
178     c.update! state: Container::Locked
179     c.update! state: Container::Running
180
181     [
182       'error', 'errorDetail', 'warning', 'warningDetail', 'activity'
183     ].each do |k|
184       # String type is allowed
185       string_val = 'A string is accepted'
186       c.update! runtime_status: {k => string_val}
187       assert_equal string_val, c.runtime_status[k]
188
189       # Other types aren't allowed
190       [
191         42, false, [], {}, nil
192       ].each do |unallowed_val|
193         assert_raises ActiveRecord::RecordInvalid do
194           c.update! runtime_status: {k => unallowed_val}
195         end
196       end
197     end
198   end
199
200   test "Container runtime_status updates" do
201     set_user_from_auth :active
202     attrs = {
203       environment: {},
204       mounts: {"BAR" => {"kind" => "FOO"}},
205       output_path: "/tmp",
206       priority: 1,
207       runtime_constraints: {"vcpus" => 1, "ram" => 1}
208     }
209     c1, _ = minimal_new(attrs)
210     assert_equal c1.runtime_status, {}
211
212     assert_equal Container::Queued, c1.state
213     assert_raises ArvadosModel::PermissionDeniedError do
214       c1.update! runtime_status: {'error' => 'Oops!'}
215     end
216
217     set_user_from_auth :system_user
218
219     # Allow updates when state = Locked
220     c1.update! state: Container::Locked
221     c1.update! runtime_status: {'error' => 'Oops!'}
222     assert c1.runtime_status.key? 'error'
223
224     # Reset when transitioning from Locked to Queued
225     c1.update! state: Container::Queued
226     assert_equal c1.runtime_status, {}
227
228     # Allow updates when state = Running
229     c1.update! state: Container::Locked
230     c1.update! state: Container::Running
231     c1.update! runtime_status: {'error' => 'Oops!'}
232     assert c1.runtime_status.key? 'error'
233
234     # Don't allow updates on other states
235     c1.update! state: Container::Complete
236     assert_raises ActiveRecord::RecordInvalid do
237       c1.update! runtime_status: {'error' => 'Some other error'}
238     end
239
240     set_user_from_auth :active
241     c2, _ = minimal_new(attrs)
242     assert_equal c2.runtime_status, {}
243     set_user_from_auth :system_user
244     c2.update! state: Container::Locked
245     c2.update! state: Container::Running
246     c2.update! state: Container::Cancelled
247     assert_raises ActiveRecord::RecordInvalid do
248       c2.update! runtime_status: {'error' => 'Oops!'}
249     end
250   end
251
252   test "Container serialized hash attributes sorted before save" do
253     set_user_from_auth :active
254     env = {"C" => "3", "B" => "2", "A" => "1"}
255     m = {"F" => {"kind" => "3"}, "E" => {"kind" => "2"}, "D" => {"kind" => "1"}}
256     rc = {"vcpus" => 1, "ram" => 1, "keep_cache_ram" => 1, "keep_cache_disk" => 0, "API" => true, "cuda" => {"device_count":0, "driver_version": "", "hardware_capability": ""}}
257     c, _ = minimal_new(environment: env, mounts: m, runtime_constraints: rc)
258     c.reload
259     assert_equal Container.deep_sort_hash(env).to_json, c.environment.to_json
260     assert_equal Container.deep_sort_hash(m).to_json, c.mounts.to_json
261     assert_equal Container.deep_sort_hash(rc).to_json, c.runtime_constraints.to_json
262   end
263
264   test 'deep_sort_hash on array of hashes' do
265     a = {'z' => [[{'a' => 'a', 'b' => 'b'}]]}
266     b = {'z' => [[{'b' => 'b', 'a' => 'a'}]]}
267     assert_equal Container.deep_sort_hash(a).to_json, Container.deep_sort_hash(b).to_json
268   end
269
270   test "find_reusable method should select higher priority queued container" do
271         Rails.configuration.Containers.LogReuseDecisions = true
272     set_user_from_auth :active
273     common_attrs = REUSABLE_COMMON_ATTRS.merge({environment:{"var" => "queued"}})
274     c_low_priority, _ = minimal_new(common_attrs.merge({use_existing:false, priority:1}))
275     c_high_priority, _ = minimal_new(common_attrs.merge({use_existing:false, priority:2}))
276     assert_not_equal c_low_priority.uuid, c_high_priority.uuid
277     assert_equal Container::Queued, c_low_priority.state
278     assert_equal Container::Queued, c_high_priority.state
279     reused = Container.find_reusable(common_attrs)
280     assert_not_nil reused
281     assert_equal reused.uuid, c_high_priority.uuid
282   end
283
284   test "find_reusable method should select latest completed container" do
285     set_user_from_auth :active
286     common_attrs = REUSABLE_COMMON_ATTRS.merge({environment: {"var" => "complete"}})
287     completed_attrs = {
288       state: Container::Complete,
289       exit_code: 0,
290       log: 'ea10d51bcf88862dbcc36eb292017dfd+45',
291       output: '1f4b0bc7583c2a7f9102c395f4ffc5e3+45'
292     }
293
294     c_older, _ = minimal_new(common_attrs.merge({use_existing: false}))
295     c_recent, _ = minimal_new(common_attrs.merge({use_existing: false}))
296     assert_not_equal c_older.uuid, c_recent.uuid
297
298     set_user_from_auth :system_user
299     c_older.update!({state: Container::Locked})
300     c_older.update!({state: Container::Running})
301     c_older.update!(completed_attrs)
302
303     c_recent.update!({state: Container::Locked})
304     c_recent.update!({state: Container::Running})
305     c_recent.update!(completed_attrs)
306
307     reused = Container.find_reusable(common_attrs)
308     assert_not_nil reused
309     assert_equal reused.uuid, c_older.uuid
310   end
311
312   test "find_reusable method should select oldest completed container when inconsistent outputs exist" do
313     set_user_from_auth :active
314     common_attrs = REUSABLE_COMMON_ATTRS.merge({environment: {"var" => "complete"}, priority: 1})
315     completed_attrs = {
316       state: Container::Complete,
317       exit_code: 0,
318       log: 'ea10d51bcf88862dbcc36eb292017dfd+45',
319     }
320
321     cr = ContainerRequest.new request_only(common_attrs)
322     cr.use_existing = false
323     cr.state = ContainerRequest::Committed
324     cr.save!
325     c_output1 = Container.where(uuid: cr.container_uuid).first
326
327     cr = ContainerRequest.new request_only(common_attrs)
328     cr.use_existing = false
329     cr.state = ContainerRequest::Committed
330     cr.save!
331     c_output2 = Container.where(uuid: cr.container_uuid).first
332
333     assert_not_equal c_output1.uuid, c_output2.uuid
334
335     set_user_from_auth :system_user
336
337     out1 = '1f4b0bc7583c2a7f9102c395f4ffc5e3+45'
338     log1 = collections(:log_collection).portable_data_hash
339     c_output1.update!({state: Container::Locked})
340     c_output1.update!({state: Container::Running})
341     c_output1.update!(completed_attrs.merge({log: log1, output: out1}))
342
343     out2 = 'fa7aeb5140e2848d39b416daeef4ffc5+45'
344     c_output2.update!({state: Container::Locked})
345     c_output2.update!({state: Container::Running})
346     c_output2.update!(completed_attrs.merge({log: log1, output: out2}))
347
348     set_user_from_auth :active
349     reused = Container.resolve(ContainerRequest.new(request_only(common_attrs)))
350     assert_equal c_output1.uuid, reused.uuid
351   end
352
353   test "find_reusable method should select running container by start date" do
354     set_user_from_auth :active
355     common_attrs = REUSABLE_COMMON_ATTRS.merge({environment: {"var" => "running"}})
356     c_slower, _ = minimal_new(common_attrs.merge({use_existing: false}))
357     c_faster_started_first, _ = minimal_new(common_attrs.merge({use_existing: false}))
358     c_faster_started_second, _ = minimal_new(common_attrs.merge({use_existing: false}))
359     # Confirm the 3 container UUIDs are different.
360     assert_equal 3, [c_slower.uuid, c_faster_started_first.uuid, c_faster_started_second.uuid].uniq.length
361     set_user_from_auth :system_user
362     c_slower.update!({state: Container::Locked})
363     c_slower.update!({state: Container::Running,
364                                  progress: 0.1})
365     c_faster_started_first.update!({state: Container::Locked})
366     c_faster_started_first.update!({state: Container::Running,
367                                                progress: 0.15})
368     c_faster_started_second.update!({state: Container::Locked})
369     c_faster_started_second.update!({state: Container::Running,
370                                                 progress: 0.15})
371     reused = Container.find_reusable(common_attrs)
372     assert_not_nil reused
373     # Selected container is the one that started first
374     assert_equal reused.uuid, c_faster_started_first.uuid
375   end
376
377   test "find_reusable method should select running container by progress" do
378     set_user_from_auth :active
379     common_attrs = REUSABLE_COMMON_ATTRS.merge({environment: {"var" => "running2"}})
380     c_slower, _ = minimal_new(common_attrs.merge({use_existing: false}))
381     c_faster_started_first, _ = minimal_new(common_attrs.merge({use_existing: false}))
382     c_faster_started_second, _ = minimal_new(common_attrs.merge({use_existing: false}))
383     # Confirm the 3 container UUIDs are different.
384     assert_equal 3, [c_slower.uuid, c_faster_started_first.uuid, c_faster_started_second.uuid].uniq.length
385     set_user_from_auth :system_user
386     c_slower.update!({state: Container::Locked})
387     c_slower.update!({state: Container::Running,
388                                  progress: 0.1})
389     c_faster_started_first.update!({state: Container::Locked})
390     c_faster_started_first.update!({state: Container::Running,
391                                                progress: 0.15})
392     c_faster_started_second.update!({state: Container::Locked})
393     c_faster_started_second.update!({state: Container::Running,
394                                                 progress: 0.2})
395     reused = Container.find_reusable(common_attrs)
396     assert_not_nil reused
397     # Selected container is the one with most progress done
398     assert_equal reused.uuid, c_faster_started_second.uuid
399   end
400
401   test "find_reusable method should select non-failing running container" do
402     set_user_from_auth :active
403     common_attrs = REUSABLE_COMMON_ATTRS.merge({environment: {"var" => "running2"}})
404     c_slower, _ = minimal_new(common_attrs.merge({use_existing: false}))
405     c_faster_started_first, _ = minimal_new(common_attrs.merge({use_existing: false}))
406     c_faster_started_second, _ = minimal_new(common_attrs.merge({use_existing: false}))
407     # Confirm the 3 container UUIDs are different.
408     assert_equal 3, [c_slower.uuid, c_faster_started_first.uuid, c_faster_started_second.uuid].uniq.length
409     set_user_from_auth :system_user
410     c_slower.update!({state: Container::Locked})
411     c_slower.update!({state: Container::Running,
412                                  progress: 0.1})
413     c_faster_started_first.update!({state: Container::Locked})
414     c_faster_started_first.update!({state: Container::Running,
415                                                runtime_status: {'warning' => 'This is not an error'},
416                                                progress: 0.15})
417     c_faster_started_second.update!({state: Container::Locked})
418     assert_equal 0, Container.where("runtime_status->'error' is not null").count
419     c_faster_started_second.update!({state: Container::Running,
420                                                 runtime_status: {'error' => 'Something bad happened'},
421                                                 progress: 0.2})
422     assert_equal 1, Container.where("runtime_status->'error' is not null").count
423     reused = Container.find_reusable(common_attrs)
424     assert_not_nil reused
425     # Selected the non-failing container even if it's the one with less progress done
426     assert_equal reused.uuid, c_faster_started_first.uuid
427   end
428
429   test "find_reusable method should select locked container most likely to start sooner" do
430     set_user_from_auth :active
431     common_attrs = REUSABLE_COMMON_ATTRS.merge({environment: {"var" => "locked"}})
432     c_low_priority, _ = minimal_new(common_attrs.merge({use_existing: false}))
433     c_high_priority_older, _ = minimal_new(common_attrs.merge({use_existing: false}))
434     c_high_priority_newer, _ = minimal_new(common_attrs.merge({use_existing: false}))
435     # Confirm the 3 container UUIDs are different.
436     assert_equal 3, [c_low_priority.uuid, c_high_priority_older.uuid, c_high_priority_newer.uuid].uniq.length
437     set_user_from_auth :system_user
438     c_low_priority.update!({state: Container::Locked,
439                                        priority: 1})
440     c_high_priority_older.update!({state: Container::Locked,
441                                               priority: 2})
442     c_high_priority_newer.update!({state: Container::Locked,
443                                               priority: 2})
444     reused = Container.find_reusable(common_attrs)
445     assert_not_nil reused
446     assert_equal reused.uuid, c_high_priority_older.uuid
447   end
448
449   test "find_reusable method should select running over failed container" do
450     set_user_from_auth :active
451     common_attrs = REUSABLE_COMMON_ATTRS.merge({environment: {"var" => "failed_vs_running"}})
452     c_failed, _ = minimal_new(common_attrs.merge({use_existing: false}))
453     c_running, _ = minimal_new(common_attrs.merge({use_existing: false}))
454     assert_not_equal c_failed.uuid, c_running.uuid
455     set_user_from_auth :system_user
456     c_failed.update!({state: Container::Locked})
457     c_failed.update!({state: Container::Running})
458     c_failed.update!({state: Container::Complete,
459                                  exit_code: 42,
460                                  log: 'ea10d51bcf88862dbcc36eb292017dfd+45',
461                                  output: 'ea10d51bcf88862dbcc36eb292017dfd+45'})
462     c_running.update!({state: Container::Locked})
463     c_running.update!({state: Container::Running,
464                                   progress: 0.15})
465     reused = Container.find_reusable(common_attrs)
466     assert_not_nil reused
467     assert_equal reused.uuid, c_running.uuid
468   end
469
470   test "find_reusable method should select complete over running container" do
471     set_user_from_auth :active
472     common_attrs = REUSABLE_COMMON_ATTRS.merge({environment: {"var" => "completed_vs_running"}})
473     c_completed, _ = minimal_new(common_attrs.merge({use_existing: false}))
474     c_running, _ = minimal_new(common_attrs.merge({use_existing: false}))
475     assert_not_equal c_completed.uuid, c_running.uuid
476     set_user_from_auth :system_user
477     c_completed.update!({state: Container::Locked})
478     c_completed.update!({state: Container::Running})
479     c_completed.update!({state: Container::Complete,
480                                     exit_code: 0,
481                                     log: 'ea10d51bcf88862dbcc36eb292017dfd+45',
482                                     output: '1f4b0bc7583c2a7f9102c395f4ffc5e3+45'})
483     c_running.update!({state: Container::Locked})
484     c_running.update!({state: Container::Running,
485                                   progress: 0.15})
486     reused = Container.find_reusable(common_attrs)
487     assert_not_nil reused
488     assert_equal c_completed.uuid, reused.uuid
489   end
490
491   test "find_reusable method should select running over locked container" do
492     set_user_from_auth :active
493     common_attrs = REUSABLE_COMMON_ATTRS.merge({environment: {"var" => "running_vs_locked"}})
494     c_locked, _ = minimal_new(common_attrs.merge({use_existing: false}))
495     c_running, _ = minimal_new(common_attrs.merge({use_existing: false}))
496     assert_not_equal c_running.uuid, c_locked.uuid
497     set_user_from_auth :system_user
498     c_locked.update!({state: Container::Locked})
499     c_running.update!({state: Container::Locked})
500     c_running.update!({state: Container::Running,
501                                   progress: 0.15})
502     reused = Container.find_reusable(common_attrs)
503     assert_not_nil reused
504     assert_equal reused.uuid, c_running.uuid
505   end
506
507   test "find_reusable method should select locked over queued container" do
508     set_user_from_auth :active
509     common_attrs = REUSABLE_COMMON_ATTRS.merge({environment: {"var" => "running_vs_locked"}})
510     c_locked, _ = minimal_new(common_attrs.merge({use_existing: false}))
511     c_queued, _ = minimal_new(common_attrs.merge({use_existing: false}))
512     assert_not_equal c_queued.uuid, c_locked.uuid
513     set_user_from_auth :system_user
514     c_locked.update!({state: Container::Locked})
515     reused = Container.find_reusable(common_attrs)
516     assert_not_nil reused
517     assert_equal reused.uuid, c_locked.uuid
518   end
519
520   test "find_reusable method should not select failed container" do
521     set_user_from_auth :active
522     attrs = REUSABLE_COMMON_ATTRS.merge({environment: {"var" => "failed"}})
523     c, _ = minimal_new(attrs)
524     set_user_from_auth :system_user
525     c.update!({state: Container::Locked})
526     c.update!({state: Container::Running})
527     c.update!({state: Container::Complete,
528                           exit_code: 33})
529     reused = Container.find_reusable(attrs)
530     assert_nil reused
531   end
532
533   [[false, false, true],
534    [false, true, true],
535    [true, false, false],
536    [true, true, true]
537   ].each do |c1_preemptible, c2_preemptible, should_reuse|
538     [[Container::Queued, 1],
539      [Container::Locked, 1],
540      [Container::Running, 0],   # not cancelled yet, but obviously will be soon
541     ].each do |c1_state, c1_priority|
542       test "find_reusable for #{c2_preemptible ? '' : 'non-'}preemptible req should #{should_reuse ? '' : 'not'} reuse a #{c1_state} #{c1_preemptible ? '' : 'non-'}preemptible container with priority #{c1_priority}" do
543         configure_preemptible_instance_type
544         set_user_from_auth :active
545         c1_attrs = REUSABLE_COMMON_ATTRS.merge({environment: {"test" => name, "state" => c1_state}, scheduling_parameters: {"preemptible" => c1_preemptible}})
546         c1, _ = minimal_new(c1_attrs)
547         set_user_from_auth :system_user
548         c1.update!({state: Container::Locked}) if c1_state != Container::Queued
549         c1.update!({state: Container::Running, priority: c1_priority}) if c1_state == Container::Running
550         c2_attrs = c1_attrs.merge({scheduling_parameters: {"preemptible" => c2_preemptible}})
551         reused = Container.find_reusable(c2_attrs)
552         if should_reuse && c1_priority > 0
553           assert_not_nil reused
554         else
555           assert_nil reused
556         end
557       end
558     end
559   end
560
561   test "find_reusable with logging disabled" do
562     set_user_from_auth :active
563     Rails.logger.expects(:info).never
564     Container.find_reusable(REUSABLE_COMMON_ATTRS)
565   end
566
567   test "find_reusable with logging enabled" do
568     set_user_from_auth :active
569     Rails.configuration.Containers.LogReuseDecisions = true
570     Rails.logger.expects(:info).at_least(3)
571     Container.find_reusable(REUSABLE_COMMON_ATTRS)
572   end
573
574   def runtime_token_attr tok
575     auth = api_client_authorizations(tok)
576     {runtime_user_uuid: User.find_by_id(auth.user_id).uuid,
577      runtime_auth_scopes: auth.scopes,
578      runtime_token: auth.token}
579   end
580
581   test "find_reusable method with same runtime_token" do
582     set_user_from_auth :active
583     common_attrs = REUSABLE_COMMON_ATTRS.merge({use_existing:false, priority:1, environment:{"var" => "queued"}})
584     c1, _ = minimal_new(common_attrs.merge({runtime_token: api_client_authorizations(:container_runtime_token).token}))
585     assert_equal Container::Queued, c1.state
586     reused = Container.find_reusable(common_attrs.merge(runtime_token_attr(:container_runtime_token)))
587     assert_not_nil reused
588     assert_equal reused.uuid, c1.uuid
589   end
590
591   test "find_reusable method with different runtime_token, same user" do
592     set_user_from_auth :active
593     common_attrs = REUSABLE_COMMON_ATTRS.merge({use_existing:false, priority:1, environment:{"var" => "queued"}})
594     c1, _ = minimal_new(common_attrs.merge({runtime_token: api_client_authorizations(:crt_user).token}))
595     assert_equal Container::Queued, c1.state
596     reused = Container.find_reusable(common_attrs.merge(runtime_token_attr(:container_runtime_token)))
597     assert_not_nil reused
598     assert_equal reused.uuid, c1.uuid
599   end
600
601   test "find_reusable method with nil runtime_token, then runtime_token with same user" do
602     set_user_from_auth :crt_user
603     common_attrs = REUSABLE_COMMON_ATTRS.merge({use_existing:false, priority:1, environment:{"var" => "queued"}})
604     c1, _ = minimal_new(common_attrs)
605     assert_equal Container::Queued, c1.state
606     assert_equal users(:container_runtime_token_user).uuid, c1.runtime_user_uuid
607     reused = Container.find_reusable(common_attrs.merge(runtime_token_attr(:container_runtime_token)))
608     assert_not_nil reused
609     assert_equal reused.uuid, c1.uuid
610   end
611
612   test "find_reusable method with different runtime_token, different user" do
613     set_user_from_auth :crt_user
614     common_attrs = REUSABLE_COMMON_ATTRS.merge({use_existing:false, priority:1, environment:{"var" => "queued"}})
615     c1, _ = minimal_new(common_attrs.merge({runtime_token: api_client_authorizations(:active).token}))
616     assert_equal Container::Queued, c1.state
617     reused = Container.find_reusable(common_attrs.merge(runtime_token_attr(:container_runtime_token)))
618     # See #14584
619     assert_not_nil reused
620     assert_equal c1.uuid, reused.uuid
621   end
622
623   test "find_reusable method with nil runtime_token, then runtime_token with different user" do
624     set_user_from_auth :active
625     common_attrs = REUSABLE_COMMON_ATTRS.merge({use_existing:false, priority:1, environment:{"var" => "queued"}})
626     c1, _ = minimal_new(common_attrs.merge({runtime_token: nil}))
627     assert_equal Container::Queued, c1.state
628     reused = Container.find_reusable(common_attrs.merge(runtime_token_attr(:container_runtime_token)))
629     # See #14584
630     assert_not_nil reused
631     assert_equal c1.uuid, reused.uuid
632   end
633
634   test "find_reusable method with different runtime_token, different scope, same user" do
635     set_user_from_auth :active
636     common_attrs = REUSABLE_COMMON_ATTRS.merge({use_existing:false, priority:1, environment:{"var" => "queued"}})
637     c1, _ = minimal_new(common_attrs.merge({runtime_token: api_client_authorizations(:runtime_token_limited_scope).token}))
638     assert_equal Container::Queued, c1.state
639     reused = Container.find_reusable(common_attrs.merge(runtime_token_attr(:container_runtime_token)))
640     # See #14584
641     assert_not_nil reused
642     assert_equal c1.uuid, reused.uuid
643   end
644
645   test "find_reusable method with cuda" do
646     set_user_from_auth :active
647     # No cuda
648     no_cuda_attrs = REUSABLE_COMMON_ATTRS.merge({use_existing:false, priority:1, environment:{"var" => "queued"},
649                                                 runtime_constraints: {"vcpus" => 1, "ram" => 1, "keep_cache_disk"=>0, "keep_cache_ram"=>268435456, "API" => false,
650                                                                       "cuda" => {"device_count":0, "driver_version": "", "hardware_capability": ""}},})
651     c1, _ = minimal_new(no_cuda_attrs)
652     assert_equal Container::Queued, c1.state
653
654     # has cuda
655     cuda_attrs = REUSABLE_COMMON_ATTRS.merge({use_existing:false, priority:1, environment:{"var" => "queued"},
656                                                 runtime_constraints: {"vcpus" => 1, "ram" => 1, "keep_cache_disk"=>0, "keep_cache_ram"=>268435456, "API" => false,
657                                                                       "cuda" => {"device_count":1, "driver_version": "11.0", "hardware_capability": "9.0"}},})
658     c2, _ = minimal_new(cuda_attrs)
659     assert_equal Container::Queued, c2.state
660
661     # should find the no cuda one
662     reused = Container.find_reusable(no_cuda_attrs)
663     assert_not_nil reused
664     assert_equal reused.uuid, c1.uuid
665
666     # should find the cuda one
667     reused = Container.find_reusable(cuda_attrs)
668     assert_not_nil reused
669     assert_equal reused.uuid, c2.uuid
670   end
671
672   test "Container running" do
673     set_user_from_auth :active
674     c, _ = minimal_new priority: 1
675
676     set_user_from_auth :system_user
677     check_illegal_updates c, [{state: Container::Running},
678                               {state: Container::Complete}]
679
680     c.lock
681     c.update! state: Container::Running
682
683     check_illegal_modify c
684     check_bogus_states c
685
686     check_illegal_updates c, [{state: Container::Queued}]
687     c.reload
688
689     c.update! priority: 3
690   end
691
692   test "Lock and unlock" do
693     set_user_from_auth :active
694     c, cr = minimal_new priority: 0
695
696     set_user_from_auth :system_user
697     assert_equal Container::Queued, c.state
698
699     assert_raise(ArvadosModel::LockFailedError) do
700       # "no priority"
701       c.lock
702     end
703     c.reload
704     assert cr.update priority: 1
705
706     refute c.update(state: Container::Running), "not locked"
707     c.reload
708     refute c.update(state: Container::Complete), "not locked"
709     c.reload
710
711     assert c.lock, show_errors(c)
712     assert c.locked_by_uuid
713     assert c.auth_uuid
714
715     assert_raise(ArvadosModel::LockFailedError) {c.lock}
716     c.reload
717
718     assert c.unlock, show_errors(c)
719     refute c.locked_by_uuid
720     refute c.auth_uuid
721
722     refute c.update(state: Container::Running), "not locked"
723     c.reload
724     refute c.locked_by_uuid
725     refute c.auth_uuid
726
727     assert c.lock, show_errors(c)
728     assert c.update(state: Container::Running), show_errors(c)
729     assert c.locked_by_uuid
730     assert c.auth_uuid
731
732     auth_uuid_was = c.auth_uuid
733
734     assert_raise(ArvadosModel::LockFailedError) do
735       # Running to Locked is not allowed
736       c.lock
737     end
738     c.reload
739     assert_raise(ArvadosModel::InvalidStateTransitionError) do
740       # Running to Queued is not allowed
741       c.unlock
742     end
743     c.reload
744
745     assert c.update(state: Container::Complete), show_errors(c)
746     refute c.locked_by_uuid
747     refute c.auth_uuid
748
749     auth_exp = ApiClientAuthorization.find_by_uuid(auth_uuid_was).expires_at
750     assert_operator auth_exp, :<, db_current_time
751
752     assert_nil ApiClientAuthorization.validate(token: ApiClientAuthorization.find_by_uuid(auth_uuid_was).token)
753   end
754
755   test "Exceed maximum lock-unlock cycles" do
756     Rails.configuration.Containers.MaxDispatchAttempts = 3
757
758     set_user_from_auth :active
759     c, cr = minimal_new
760
761     set_user_from_auth :system_user
762     assert_equal Container::Queued, c.state
763     assert_equal 0, c.lock_count
764
765     c.lock
766     c.reload
767     assert_equal 1, c.lock_count
768     assert_equal Container::Locked, c.state
769
770     c.unlock
771     c.reload
772     assert_equal 1, c.lock_count
773     assert_equal Container::Queued, c.state
774
775     c.lock
776     c.reload
777     assert_equal 2, c.lock_count
778     assert_equal Container::Locked, c.state
779
780     c.unlock
781     c.reload
782     assert_equal 2, c.lock_count
783     assert_equal Container::Queued, c.state
784
785     c.lock
786     c.reload
787     assert_equal 3, c.lock_count
788     assert_equal Container::Locked, c.state
789
790     c.unlock
791     c.reload
792     assert_equal 3, c.lock_count
793     assert_equal Container::Cancelled, c.state
794
795     assert_raise(ArvadosModel::LockFailedError) do
796       # Cancelled to Locked is not allowed
797       c.lock
798     end
799   end
800
801   test "Container queued cancel" do
802     set_user_from_auth :active
803     c, cr = minimal_new({container_count_max: 1})
804     set_user_from_auth :system_user
805     assert c.update(state: Container::Cancelled), show_errors(c)
806     check_no_change_from_cancelled c
807     cr.reload
808     assert_equal ContainerRequest::Final, cr.state
809   end
810
811   test "Container queued count" do
812     assert_equal 1, Container.readable_by(users(:active)).where(state: "Queued").count
813   end
814
815   test "Containers with no matching request are readable by admin" do
816     uuids = Container.includes('container_requests').where(container_requests: {uuid: nil}).collect(&:uuid)
817     assert_not_empty uuids
818     assert_empty Container.readable_by(users(:active)).where(uuid: uuids)
819     assert_not_empty Container.readable_by(users(:admin)).where(uuid: uuids)
820     assert_equal uuids.count, Container.readable_by(users(:admin)).where(uuid: uuids).count
821   end
822
823   test "Container locked cancel" do
824     set_user_from_auth :active
825     c, _ = minimal_new
826     set_user_from_auth :system_user
827     assert c.lock, show_errors(c)
828     assert c.update(state: Container::Cancelled), show_errors(c)
829     check_no_change_from_cancelled c
830   end
831
832   test "Container locked with non-expiring token" do
833     Rails.configuration.API.TokenMaxLifetime = 1.hour
834     set_user_from_auth :active
835     c, _ = minimal_new
836     set_user_from_auth :system_user
837     assert c.lock, show_errors(c)
838     refute c.auth.nil?
839     assert c.auth.expires_at.nil?
840     assert c.auth.user_id == User.find_by_uuid(users(:active).uuid).id
841   end
842
843   test "Container locked cancel with log" do
844     set_user_from_auth :active
845     c, _ = minimal_new
846     set_user_from_auth :system_user
847     assert c.lock, show_errors(c)
848     assert c.update(
849              state: Container::Cancelled,
850              log: collections(:log_collection).portable_data_hash,
851            ), show_errors(c)
852     check_no_change_from_cancelled c
853   end
854
855   test "Container running cancel" do
856     set_user_from_auth :active
857     c, _ = minimal_new
858     set_user_from_auth :system_user
859     c.lock
860     c.update! state: Container::Running
861     c.update! state: Container::Cancelled
862     check_no_change_from_cancelled c
863   end
864
865   test "Container create forbidden for non-admin" do
866     set_user_from_auth :active_trustedclient
867     c = Container.new DEFAULT_ATTRS
868     c.environment = {}
869     c.mounts = {"BAR" => "FOO"}
870     c.output_path = "/tmp"
871     c.priority = 1
872     c.runtime_constraints = {}
873     assert_raises(ArvadosModel::PermissionDeniedError) do
874       c.save!
875     end
876   end
877
878   [
879     [Container::Queued, {state: Container::Locked}],
880     [Container::Queued, {state: Container::Running}],
881     [Container::Queued, {state: Container::Complete}],
882     [Container::Queued, {state: Container::Cancelled}],
883     [Container::Queued, {priority: 123456789}],
884     [Container::Queued, {runtime_status: {'error' => 'oops'}}],
885     [Container::Queued, {cwd: '/'}],
886     [Container::Locked, {state: Container::Running}],
887     [Container::Locked, {state: Container::Queued}],
888     [Container::Locked, {priority: 123456789}],
889     [Container::Locked, {runtime_status: {'error' => 'oops'}}],
890     [Container::Locked, {cwd: '/'}],
891     [Container::Running, {state: Container::Complete}],
892     [Container::Running, {state: Container::Cancelled}],
893     [Container::Running, {priority: 123456789}],
894     [Container::Running, {runtime_status: {'error' => 'oops'}}],
895     [Container::Running, {cwd: '/'}],
896     [Container::Running, {gateway_address: "172.16.0.1:12345"}],
897     [Container::Running, {interactive_session_started: true}],
898     [Container::Complete, {state: Container::Cancelled}],
899     [Container::Complete, {priority: 123456789}],
900     [Container::Complete, {runtime_status: {'error' => 'oops'}}],
901     [Container::Complete, {cwd: '/'}],
902     [Container::Cancelled, {cwd: '/'}],
903   ].each do |start_state, updates|
904     test "Container update #{updates.inspect} when #{start_state} forbidden for non-admin" do
905       set_user_from_auth :active
906       c, _ = minimal_new
907       if start_state != Container::Queued
908         set_user_from_auth :system_user
909         c.lock
910         if start_state != Container::Locked
911           c.update! state: Container::Running
912           if start_state != Container::Running
913             c.update! state: start_state
914           end
915         end
916       end
917       assert_equal c.state, start_state
918       set_user_from_auth :active
919       assert_raises(ArvadosModel::PermissionDeniedError) do
920         c.update! updates
921       end
922     end
923   end
924
925   test "can only change exit code while running and at completion" do
926     set_user_from_auth :active
927     c, _ = minimal_new
928     set_user_from_auth :system_user
929     c.lock
930     check_illegal_updates c, [{exit_code: 1}]
931     c.update! state: Container::Running
932     assert c.update(exit_code: 1)
933     assert c.update(exit_code: 1, state: Container::Complete)
934   end
935
936   test "locked_by_uuid can update log when locked/running, and output when running" do
937     set_user_from_auth :active
938     logcoll = collections(:container_log_collection)
939     c, cr1 = minimal_new
940     cr2 = ContainerRequest.new(DEFAULT_ATTRS)
941     cr2.state = ContainerRequest::Committed
942     act_as_user users(:active) do
943       cr2.save!
944     end
945     assert_equal cr1.container_uuid, cr2.container_uuid
946
947     logpdh_time1 = logcoll.portable_data_hash
948
949     set_user_from_auth :system_user
950     c.lock
951     assert_equal c.locked_by_uuid, Thread.current[:api_client_authorization].uuid
952     c.update!(log: logpdh_time1)
953     c.update!(state: Container::Running)
954     cr1.reload
955     cr2.reload
956     cr1log_uuid = cr1.log_uuid
957     cr2log_uuid = cr2.log_uuid
958     assert_not_nil cr1log_uuid
959     assert_not_nil cr2log_uuid
960     assert_not_equal logcoll.uuid, cr1log_uuid
961     assert_not_equal logcoll.uuid, cr2log_uuid
962     assert_not_equal cr1log_uuid, cr2log_uuid
963
964     logcoll.update!(manifest_text: logcoll.manifest_text + ". acbd18db4cc2f85cedef654fccc4a4d8+3 0:3:foo.txt\n")
965     logpdh_time2 = logcoll.portable_data_hash
966
967     assert c.update(output: collections(:collection_owned_by_active).portable_data_hash)
968     assert c.update(log: logpdh_time2)
969     assert c.update(state: Container::Complete, log: logcoll.portable_data_hash)
970     c.reload
971     assert_equal collections(:collection_owned_by_active).portable_data_hash, c.output
972     assert_equal logpdh_time2, c.log
973     refute c.update(output: nil)
974     refute c.update(log: nil)
975     cr1.reload
976     cr2.reload
977     assert_equal cr1log_uuid, cr1.log_uuid
978     assert_equal cr2log_uuid, cr2.log_uuid
979     assert_equal 1, Collection.where(uuid: [cr1log_uuid, cr2log_uuid]).to_a.collect(&:portable_data_hash).uniq.length
980     assert_equal ". 8c12f5f5297b7337598170c6f531fcee+7882 acbd18db4cc2f85cedef654fccc4a4d8+3 0:0:arv-mount.txt 0:1910:container.json 1910:1264:crunch-run.txt 3174:1005:crunchstat.txt 7882:3:foo.txt 4179:659:hoststat.txt 4838:2811:node-info.txt 7649:233:node.json 0:0:stderr.txt
981 ./log\\040for\\040container\\040#{cr1.container_uuid} 8c12f5f5297b7337598170c6f531fcee+7882 acbd18db4cc2f85cedef654fccc4a4d8+3 0:0:arv-mount.txt 0:1910:container.json 1910:1264:crunch-run.txt 3174:1005:crunchstat.txt 7882:3:foo.txt 4179:659:hoststat.txt 4838:2811:node-info.txt 7649:233:node.json 0:0:stderr.txt
982 ", Collection.find_by_uuid(cr1log_uuid).manifest_text
983   end
984
985   ["auth_uuid", "runtime_token"].each do |tok|
986     test "#{tok} can set output, progress, runtime_status, state, exit_code on running container -- but not log" do
987       if tok == "runtime_token"
988         set_user_from_auth :spectator
989         c, _ = minimal_new(container_image: "9ae44d5792468c58bcf85ce7353c7027+124",
990                            runtime_token: api_client_authorizations(:active).token)
991       else
992         set_user_from_auth :active
993         c, _ = minimal_new
994       end
995       set_user_from_auth :system_user
996       c.lock
997       c.update! state: Container::Running
998
999       if tok == "runtime_token"
1000         auth = ApiClientAuthorization.validate(token: c.runtime_token)
1001         Thread.current[:api_client_authorization] = auth
1002         Thread.current[:token] = auth.token
1003         Thread.current[:user] = auth.user
1004       else
1005         auth = ApiClientAuthorization.find_by_uuid(c.auth_uuid)
1006         Thread.current[:api_client_authorization] = auth
1007         Thread.current[:token] = auth.token
1008         Thread.current[:user] = auth.user
1009       end
1010
1011       assert c.update(gateway_address: "127.0.0.1:9")
1012       assert c.update(output: collections(:collection_owned_by_active).portable_data_hash)
1013       assert c.update(runtime_status: {'warning' => 'something happened'})
1014       assert c.update(progress: 0.5)
1015       assert c.update(exit_code: 0)
1016       refute c.update(log: collections(:log_collection).portable_data_hash)
1017       c.reload
1018       assert c.update(state: Container::Complete, exit_code: 0)
1019     end
1020   end
1021
1022   test "not allowed to set output that is not readable by current user" do
1023     set_user_from_auth :active
1024     c, _ = minimal_new
1025     set_user_from_auth :system_user
1026     c.lock
1027     c.update! state: Container::Running
1028
1029     Thread.current[:api_client_authorization] = ApiClientAuthorization.find_by_uuid(c.auth_uuid)
1030     Thread.current[:user] = User.find_by_id(Thread.current[:api_client_authorization].user_id)
1031
1032     assert_raises ActiveRecord::RecordInvalid do
1033       c.update! output: collections(:collection_not_readable_by_active).portable_data_hash
1034     end
1035   end
1036
1037   test "other token cannot set output on running container" do
1038     set_user_from_auth :active
1039     c, _ = minimal_new
1040     set_user_from_auth :system_user
1041     c.lock
1042     c.update! state: Container::Running
1043
1044     set_user_from_auth :running_to_be_deleted_container_auth
1045     assert_raises(ArvadosModel::PermissionDeniedError) do
1046       c.update(output: collections(:foo_file).portable_data_hash)
1047     end
1048   end
1049
1050   test "can set trashed output on running container" do
1051     set_user_from_auth :active
1052     c, _ = minimal_new
1053     set_user_from_auth :system_user
1054     c.lock
1055     c.update! state: Container::Running
1056
1057     output = Collection.find_by_uuid('zzzzz-4zz18-mto52zx1s7sn3jk')
1058
1059     assert output.is_trashed
1060     assert c.update output: output.portable_data_hash
1061     assert c.update! state: Container::Complete
1062   end
1063
1064   test "not allowed to set trashed output that is not readable by current user" do
1065     set_user_from_auth :active
1066     c, _ = minimal_new
1067     set_user_from_auth :system_user
1068     c.lock
1069     c.update! state: Container::Running
1070
1071     output = Collection.find_by_uuid('zzzzz-4zz18-mto52zx1s7sn3jr')
1072
1073     Thread.current[:api_client_authorization] = ApiClientAuthorization.find_by_uuid(c.auth_uuid)
1074     Thread.current[:user] = User.find_by_id(Thread.current[:api_client_authorization].user_id)
1075
1076     assert_raises ActiveRecord::RecordInvalid do
1077       c.update! output: output.portable_data_hash
1078     end
1079   end
1080
1081   test "user cannot delete" do
1082     set_user_from_auth :active
1083     c, _ = minimal_new
1084     assert_raises ArvadosModel::PermissionDeniedError do
1085       c.destroy
1086     end
1087     assert Container.find_by_uuid(c.uuid)
1088   end
1089
1090   [
1091     {state: Container::Complete, exit_code: 0, output: '1f4b0bc7583c2a7f9102c395f4ffc5e3+45'},
1092     {state: Container::Cancelled},
1093   ].each do |final_attrs|
1094     test "secret_mounts and runtime_token are null after container is #{final_attrs[:state]}" do
1095       set_user_from_auth :active
1096       c, cr = minimal_new(secret_mounts: {'/secret' => {'kind' => 'text', 'content' => 'foo'}},
1097                           container_count_max: 1, runtime_token: api_client_authorizations(:active).token)
1098       set_user_from_auth :system_user
1099       c.lock
1100       c.update!(state: Container::Running)
1101       c.reload
1102       assert c.secret_mounts.has_key?('/secret')
1103       assert_equal api_client_authorizations(:active).token, c.runtime_token
1104
1105       c.update!(final_attrs)
1106       c.reload
1107       assert_equal({}, c.secret_mounts)
1108       assert_nil c.runtime_token
1109       cr.reload
1110       assert_equal({}, cr.secret_mounts)
1111       assert_nil cr.runtime_token
1112       assert_no_secrets_logged
1113     end
1114   end
1115
1116   def configure_preemptible_instance_type
1117     Rails.configuration.InstanceTypes = ConfigLoader.to_OrderedOptions({
1118       "a1.small.pre" => {
1119         "Preemptible" => true,
1120         "Price" => 0.1,
1121         "ProviderType" => "a1.small",
1122         "VCPUs" => 1,
1123         "RAM" => 1000000000,
1124       },
1125     })
1126   end
1127
1128   def vary_parameters(**kwargs)
1129     # kwargs is a hash that maps parameters to an array of values.
1130     # This function enumerates every possible hash where each key has one of
1131     # the values from its array.
1132     # The output keys are strings since that's what container hash attributes
1133     # want.
1134     # A nil value yields a hash without that key.
1135     [[:_, nil]].product(
1136       *kwargs.map { |(key, values)| [key.to_s].product(values) },
1137     ).map { |param_pairs| Hash[param_pairs].compact }
1138   end
1139
1140   def retry_with_scheduling_parameters(param_hashes)
1141     set_user_from_auth :admin
1142     containers = {}
1143     requests = []
1144     param_hashes.each do |scheduling_parameters|
1145       container, request = minimal_new(scheduling_parameters: scheduling_parameters)
1146       containers[container.uuid] = container
1147       requests << request
1148     end
1149     refute(containers.empty?, "buggy test: no scheduling parameters enumerated")
1150     assert_equal(1, containers.length)
1151     _, container1 = containers.shift
1152     container1.lock
1153     container1.update!(state: Container::Cancelled)
1154     container1.reload
1155     request1 = requests.shift
1156     request1.reload
1157     assert_not_equal(container1.uuid, request1.container_uuid)
1158     requests.each do |request|
1159       request.reload
1160       assert_equal(request1.container_uuid, request.container_uuid)
1161     end
1162     container2 = Container.find_by_uuid(request1.container_uuid)
1163     assert_not_nil(container2)
1164     return container2
1165   end
1166
1167   preemptible_values = [true, false, nil]
1168   preemptible_values.permutation(1).chain(
1169     preemptible_values.product(preemptible_values),
1170     preemptible_values.product(preemptible_values, preemptible_values),
1171   ).each do |preemptible_a|
1172     # If the first req has preemptible=true but a subsequent req
1173     # doesn't, we want to avoid reusing the first container, so this
1174     # test isn't appropriate.
1175     next if preemptible_a[0] &&
1176             ((preemptible_a.length > 1 && !preemptible_a[1]) ||
1177              (preemptible_a.length > 2 && !preemptible_a[2]))
1178     test "retry requests scheduled with preemptible=#{preemptible_a}" do
1179       configure_preemptible_instance_type
1180       param_hashes = vary_parameters(preemptible: preemptible_a)
1181       container = retry_with_scheduling_parameters(param_hashes)
1182       assert_equal(preemptible_a.all?,
1183                    container.scheduling_parameters["preemptible"] || false)
1184     end
1185   end
1186
1187   partition_values = [nil, [], ["alpha"], ["alpha", "bravo"], ["bravo", "charlie"]]
1188   partition_values.permutation(1).chain(
1189     partition_values.permutation(2),
1190   ).each do |partitions_a|
1191     test "retry requests scheduled with partitions=#{partitions_a}" do
1192       param_hashes = vary_parameters(partitions: partitions_a)
1193       container = retry_with_scheduling_parameters(param_hashes)
1194       expected = if partitions_a.any? { |value| value.nil? or value.empty? }
1195                    []
1196                  else
1197                    partitions_a.flatten.uniq
1198                  end
1199       actual = container.scheduling_parameters["partitions"] || []
1200       assert_equal(expected.sort, actual.sort)
1201     end
1202   end
1203
1204   runtime_values = [nil, 0, 1, 2, 3]
1205   runtime_values.permutation(1).chain(
1206     runtime_values.permutation(2),
1207     runtime_values.permutation(3),
1208   ).each do |max_run_time_a|
1209     test "retry requests scheduled with max_run_time=#{max_run_time_a}" do
1210       param_hashes = vary_parameters(max_run_time: max_run_time_a)
1211       container = retry_with_scheduling_parameters(param_hashes)
1212       expected = if max_run_time_a.any? { |value| value.nil? or value == 0 }
1213                    0
1214                  else
1215                    max_run_time_a.max
1216                  end
1217       actual = container.scheduling_parameters["max_run_time"] || 0
1218       assert_equal(expected, actual)
1219     end
1220   end
1221
1222   test "retry requests with multi-varied scheduling parameters" do
1223     configure_preemptible_instance_type
1224     param_hashes = [{
1225                      "partitions": ["alpha", "bravo"],
1226                      "preemptible": false,
1227                      "max_run_time": 10,
1228                     }, {
1229                      "partitions": ["alpha", "charlie"],
1230                      "max_run_time": 20,
1231                     }, {
1232                      "partitions": ["bravo", "charlie"],
1233                      "preemptible": true,
1234                      "max_run_time": 30,
1235                     }]
1236     container = retry_with_scheduling_parameters(param_hashes)
1237     actual = container.scheduling_parameters
1238     assert_equal(["alpha", "bravo", "charlie"], actual["partitions"]&.sort)
1239     assert_equal(false, actual["preemptible"] || false)
1240     assert_equal(30, actual["max_run_time"])
1241   end
1242
1243   test "retry requests with unset scheduling parameters" do
1244     configure_preemptible_instance_type
1245     param_hashes = vary_parameters(
1246       preemptible: [nil, true],
1247       partitions: [nil, ["alpha"]],
1248       max_run_time: [nil, 5],
1249     )
1250     container = retry_with_scheduling_parameters(param_hashes)
1251     actual = container.scheduling_parameters
1252     assert_equal([], actual["partitions"] || [])
1253     assert_equal(false, actual["preemptible"] || false)
1254     assert_equal(0, actual["max_run_time"] || 0)
1255   end
1256
1257   test "retry requests with default scheduling parameters" do
1258     configure_preemptible_instance_type
1259     param_hashes = vary_parameters(
1260       preemptible: [false, true],
1261       partitions: [[], ["bravo"]],
1262       max_run_time: [0, 1],
1263     )
1264     container = retry_with_scheduling_parameters(param_hashes)
1265     actual = container.scheduling_parameters
1266     assert_equal([], actual["partitions"] || [])
1267     assert_equal(false, actual["preemptible"] || false)
1268     assert_equal(0, actual["max_run_time"] || 0)
1269   end
1270
1271   def run_container(request_params, final_attrs)
1272     final_attrs[:state] ||= Container::Complete
1273     if final_attrs[:state] == Container::Complete
1274       final_attrs[:exit_code] ||= 0
1275       final_attrs[:log] ||= collections(:log_collection).portable_data_hash
1276       final_attrs[:output] ||= collections(:multilevel_collection_1).portable_data_hash
1277     end
1278     container, request = minimal_new(request_params)
1279     container.lock
1280     container.update!(state: Container::Running)
1281     container.update!(final_attrs)
1282     return container, request
1283   end
1284
1285   def check_reuse_with_variations(default_keep_cache_ram, vary_attr, start_value, variations)
1286     container_params = REUSABLE_ATTRS_SLIM.merge(vary_attr => start_value)
1287     orig_default = Rails.configuration.Containers.DefaultKeepCacheRAM
1288     begin
1289       Rails.configuration.Containers.DefaultKeepCacheRAM = default_keep_cache_ram
1290       set_user_from_auth :admin
1291       expected, _ = run_container(container_params, {})
1292       variations.each do |variation|
1293         full_variation = REUSABLE_ATTRS_SLIM[vary_attr].merge(variation)
1294         parameters = REUSABLE_ATTRS_SLIM.merge(vary_attr => full_variation)
1295         actual = Container.find_reusable(parameters)
1296         assert_equal(expected.uuid, actual&.uuid,
1297                      "request with #{vary_attr}=#{variation} did not reuse container")
1298       end
1299     ensure
1300       Rails.configuration.Containers.DefaultKeepCacheRAM = orig_default
1301     end
1302   end
1303
1304   # Test that we can reuse a container with a known keep_cache_ram constraint,
1305   # no matter what keep_cache_* constraints the new request uses.
1306   [0, 2 << 30, 4 << 30].product(
1307     [0, 1],
1308     [true, false],
1309   ).each do |(default_keep_cache_ram, multiplier, keep_disk_constraint)|
1310     test "reuse request with DefaultKeepCacheRAM=#{default_keep_cache_ram}, keep_cache_ram*=#{multiplier}, keep_cache_disk=#{keep_disk_constraint}" do
1311       runtime_constraints = REUSABLE_ATTRS_SLIM[:runtime_constraints].merge(
1312         "keep_cache_ram" => default_keep_cache_ram * multiplier,
1313       )
1314       if not keep_disk_constraint
1315         # Simulate a container that predates keep_cache_disk by deleting
1316         # the constraint entirely.
1317         runtime_constraints.delete("keep_cache_disk")
1318       end
1319       # Important values are:
1320       # * 0
1321       # * 2GiB, the minimum default keep_cache_disk
1322       # * 8GiB, the default keep_cache_disk based on container ram
1323       # * 32GiB, the maximum default keep_cache_disk
1324       # Check these values and values in between.
1325       vary_values = [0, 1, 2, 6, 8, 10, 32, 33].map { |v| v << 30 }.to_a
1326       variations = vary_parameters(keep_cache_ram: vary_values)
1327                      .chain(vary_parameters(keep_cache_disk: vary_values))
1328       check_reuse_with_variations(
1329         default_keep_cache_ram,
1330         :runtime_constraints,
1331         runtime_constraints,
1332         variations,
1333       )
1334     end
1335   end
1336
1337   # Test that we can reuse a container with a known keep_cache_disk constraint,
1338   # no matter what keep_cache_* constraints the new request uses.
1339   # keep_cache_disk values are the important values discussed in the test above.
1340   [0, 2 << 30, 4 << 30]
1341     .product([0, 2 << 30, 8 << 30, 32 << 30])
1342     .each do |(default_keep_cache_ram, keep_cache_disk)|
1343     test "reuse request with DefaultKeepCacheRAM=#{default_keep_cache_ram} and keep_cache_disk=#{keep_cache_disk}" do
1344       runtime_constraints = REUSABLE_ATTRS_SLIM[:runtime_constraints].merge(
1345         "keep_cache_disk" => keep_cache_disk,
1346       )
1347       vary_values = [0, 1, 2, 6, 8, 10, 32, 33].map { |v| v << 30 }.to_a
1348       variations = vary_parameters(keep_cache_ram: vary_values)
1349                      .chain(vary_parameters(keep_cache_disk: vary_values))
1350       check_reuse_with_variations(
1351         default_keep_cache_ram,
1352         :runtime_constraints,
1353         runtime_constraints,
1354         variations,
1355       )
1356     end
1357   end
1358
1359   # Test that a container request can reuse a container with an exactly
1360   # matching keep_cache_* constraint, no matter what the defaults.
1361   [0, 2 << 30, 4 << 30].product(
1362     ["keep_cache_disk", "keep_cache_ram"],
1363     [135790, 13 << 30, 135 << 30],
1364   ).each do |(default_keep_cache_ram, constraint_key, constraint_value)|
1365     test "reuse request with #{constraint_key}=#{constraint_value} and DefaultKeepCacheRAM=#{default_keep_cache_ram}" do
1366       runtime_constraints = REUSABLE_ATTRS_SLIM[:runtime_constraints].merge(
1367         constraint_key => constraint_value,
1368       )
1369       check_reuse_with_variations(
1370         default_keep_cache_ram,
1371         :runtime_constraints,
1372         runtime_constraints,
1373         [runtime_constraints],
1374       )
1375     end
1376   end
1377 end