X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/c14246b9a21d038fc6fa850f4032659a98397784..77d9c05d89dabc9e9e9a15f46cd12c8ad61ed64e:/services/api/app/models/container.rb diff --git a/services/api/app/models/container.rb b/services/api/app/models/container.rb index 52f1cba723..9420ef3cb8 100644 --- a/services/api/app/models/container.rb +++ b/services/api/app/models/container.rb @@ -1,4 +1,5 @@ require 'whitelist_update' +require 'safe_json' class Container < ArvadosModel include HasUuid @@ -81,45 +82,117 @@ class Container < ArvadosModel end end + # Create a new container (or find an existing one) to satisfy the + # given container request. + def self.resolve(req) + c_attrs = { + command: req.command, + cwd: req.cwd, + environment: req.environment, + output_path: req.output_path, + container_image: resolve_container_image(req.container_image), + mounts: resolve_mounts(req.mounts), + runtime_constraints: resolve_runtime_constraints(req.runtime_constraints), + scheduling_parameters: req.scheduling_parameters, + } + act_as_system_user do + if req.use_existing && (reusable = find_reusable(c_attrs)) + reusable + else + Container.create!(c_attrs) + end + end + end + + # Return a runtime_constraints hash that complies with requested but + # is suitable for saving in a container record, i.e., has specific + # values instead of ranges. + # + # Doing this as a step separate from other resolutions, like "git + # revision range to commit hash", makes sense only when there is no + # opportunity to reuse an existing container (e.g., container reuse + # is not implemented yet, or we have already found that no existing + # containers are suitable). + def self.resolve_runtime_constraints(runtime_constraints) + rc = {} + defaults = { + 'keep_cache_ram' => + Rails.configuration.container_default_keep_cache_ram, + } + defaults.merge(runtime_constraints).each do |k, v| + if v.is_a? Array + rc[k] = v[0] + else + rc[k] = v + end + end + rc + end + + # Return a mounts hash suitable for a Container, i.e., with every + # readonly collection UUID resolved to a PDH. + def self.resolve_mounts(mounts) + c_mounts = {} + mounts.each do |k, mount| + mount = mount.dup + c_mounts[k] = mount + if mount['kind'] != 'collection' + next + end + if (uuid = mount.delete 'uuid') + c = Collection. + readable_by(current_user). + where(uuid: uuid). + select(:portable_data_hash). + first + if !c + raise ArvadosModel::UnresolvableContainerError.new "cannot mount collection #{uuid.inspect}: not found" + end + if mount['portable_data_hash'].nil? + # PDH not supplied by client + mount['portable_data_hash'] = c.portable_data_hash + elsif mount['portable_data_hash'] != c.portable_data_hash + # UUID and PDH supplied by client, but they don't agree + raise ArgumentError.new "cannot mount collection #{uuid.inspect}: current portable_data_hash #{c.portable_data_hash.inspect} does not match #{c['portable_data_hash'].inspect} in request" + end + end + end + return c_mounts + end + + # Return a container_image PDH suitable for a Container. + def self.resolve_container_image(container_image) + coll = Collection.for_latest_docker_image(container_image) + if !coll + raise ArvadosModel::UnresolvableContainerError.new "docker image #{container_image.inspect} not found" + end + coll.portable_data_hash + end + def self.find_reusable(attrs) candidates = Container. - where('command = ?', attrs[:command].to_yaml). + where_serialized(:command, attrs[:command]). where('cwd = ?', attrs[:cwd]). - where('environment = ?', self.deep_sort_hash(attrs[:environment]).to_yaml). + where_serialized(:environment, attrs[:environment]). where('output_path = ?', attrs[:output_path]). - where('container_image = ?', attrs[:container_image]). - where('mounts = ?', self.deep_sort_hash(attrs[:mounts]).to_yaml). - where('runtime_constraints = ?', self.deep_sort_hash(attrs[:runtime_constraints]).to_yaml) - - # Check for Completed candidates that had consistent outputs. - completed = candidates.where(state: Complete).where(exit_code: 0) - outputs = completed.select('output').group('output').limit(2) - if outputs.count.count != 1 - Rails.logger.debug("Found #{outputs.count.length} different outputs") - elsif Collection. - readable_by(current_user). - where(portable_data_hash: outputs.first.output). - count < 1 - Rails.logger.info("Found reusable container(s) " + - "but output #{outputs.first} is not readable " + - "by user #{current_user.uuid}") - else - # Return the oldest eligible container whose log is still - # present and readable by current_user. - readable_pdh = Collection. - readable_by(current_user). - select('portable_data_hash') - completed = completed. - where("log in (#{readable_pdh.to_sql})"). - order('finished_at asc'). - limit(1) - if completed.first - return completed.first - else - Rails.logger.info("Found reusable container(s) but none with a log " + - "readable by user #{current_user.uuid}") - end - end + where('container_image = ?', resolve_container_image(attrs[:container_image])). + where_serialized(:mounts, resolve_mounts(attrs[:mounts])). + where_serialized(:runtime_constraints, resolve_runtime_constraints(attrs[:runtime_constraints])) + + # Check for Completed candidates whose output and log are both readable. + select_readable_pdh = Collection. + readable_by(current_user). + select(:portable_data_hash). + to_sql + usable = candidates. + where(state: Complete). + where(exit_code: 0). + where("log IN (#{select_readable_pdh})"). + where("output IN (#{select_readable_pdh})"). + order('finished_at ASC'). + limit(1). + first + return usable if usable # Check for Running candidates and return the most likely to finish sooner. running = candidates.where(state: Running). @@ -284,10 +357,12 @@ class Container < ArvadosModel # that a container cannot "claim" a collection that it doesn't otherwise # have access to just by setting the output field to the collection PDH. if output_changed? - c = Collection. - readable_by(current_user). - where(portable_data_hash: self.output). - first + c = Collection.unscoped do + Collection. + readable_by(current_user). + where(portable_data_hash: self.output). + first + end if !c errors.add :output, "collection must exist and be readable by current user." end @@ -341,7 +416,7 @@ class Container < ArvadosModel act_as_system_user do if self.state == Cancelled - retryable_requests = ContainerRequest.where("priority > 0 and state = 'Committed' and container_count < container_count_max") + retryable_requests = ContainerRequest.where("container_uuid = ? and priority > 0 and state = 'Committed' and container_count < container_count_max", uuid) else retryable_requests = [] end