[ARVADOS] created: 285323e01f41e684a5ca2c972cc4357e3af3701c
Git user
git at public.curoverse.com
Wed May 17 10:12:15 EDT 2017
at 285323e01f41e684a5ca2c972cc4357e3af3701c (commit)
commit 285323e01f41e684a5ca2c972cc4357e3af3701c
Author: Tom Clegg <tom at curoverse.com>
Date: Wed May 17 10:11:55 2017 -0400
11546: Avoid loading/saving non-essential fields in /arvados/v1/containers/lock.
diff --git a/services/api/app/controllers/arvados/v1/containers_controller.rb b/services/api/app/controllers/arvados/v1/containers_controller.rb
index 51f15ad..00d161f 100644
--- a/services/api/app/controllers/arvados/v1/containers_controller.rb
+++ b/services/api/app/controllers/arvados/v1/containers_controller.rb
@@ -24,8 +24,18 @@ class Arvados::V1::ContainersController < ApplicationController
end
end
+ def find_objects_for_index
+ if action_name == 'lock' || action_name == 'unlock'
+ # Avoid loading more fields than we need
+ @select = %w(uuid state auth_uuid locked_by_uuid)
+ end
+ super
+ end
+
def lock
- @object.lock
+ Container.lock_container(uuid: @object.uuid)
+ # Reload (but still only the essential fields)
+ find_object_by_uuid
show
end
diff --git a/services/api/app/models/arvados_model.rb b/services/api/app/models/arvados_model.rb
index 89f9a88..9a19b48 100644
--- a/services/api/app/models/arvados_model.rb
+++ b/services/api/app/models/arvados_model.rb
@@ -46,6 +46,12 @@ class ArvadosModel < ActiveRecord::Base
end
end
+ class LockFailedError < StandardError
+ def http_status
+ 422
+ end
+ end
+
class InvalidStateTransitionError < StandardError
def http_status
422
@@ -98,6 +104,11 @@ class ArvadosModel < ActiveRecord::Base
super(self.class.permit_attribute_params(raw_params), *args)
end
+ def reload(*args)
+ super
+ log_start_state
+ end
+
def self.create raw_params={}, *args
super(permit_attribute_params(raw_params), *args)
end
diff --git a/services/api/app/models/container.rb b/services/api/app/models/container.rb
index 15a9c50..f027001 100644
--- a/services/api/app/models/container.rb
+++ b/services/api/app/models/container.rb
@@ -7,6 +7,7 @@ class Container < ArvadosModel
include CommonApiTemplate
include WhitelistUpdate
extend CurrentApiClient
+ extend DbCurrentTime
serialize :environment, Hash
serialize :mounts, Hash
@@ -212,23 +213,80 @@ class Container < ArvadosModel
nil
end
- def lock
- with_lock do
- if self.state == Locked
- raise AlreadyLockedError
+ def lock_and_reload
+ self.class.lock_container(uuid: self.uuid)
+ self.reload
+ end
+
+ # This class method should be used instead of the instance method
+ # when possible. It avoids a "reload entire AR object" operation,
+ # which can be expensive -- especially when it occurs inside a
+ # transaction with uncommitted writes, in which case it can block
+ # other readers.
+ def self.lock_container(uuid:)
+ c = select(:id, :uuid, :state, :updated_at,
+ :modified_at, :modified_by_user_uuid,
+ :owner_uuid).
+ where('uuid=? and priority>0', uuid).
+ first
+ if !c
+ raise ActiveRecord::RecordNotFound
+ end
+
+ if c.state == Locked
+ raise LockFailedError.new("cannot lock: already locked")
+ elsif c.state != Queued
+ raise InvalidStateTransitionError
+ end
+ cr = ContainerRequest.
+ where('container_uuid=? and priority>0', uuid).
+ order('priority desc').
+ first
+ if !cr
+ raise LockFailedError.new("cannot lock: priority <= 0")
+ end
+
+ locked_by_uuid = current_api_client_authorization.uuid
+
+ transaction do
+ # Create a new auth token for the container. This will be rolled
+ # back if we end up raising LockFailedError.
+ auth_uuid = ApiClientAuthorization.
+ create!(user_id: User.find_by_uuid(cr.modified_by_user_uuid).id,
+ api_client_id: 0).
+ uuid
+
+ now = db_current_time.utc.iso8601(9)
+ r = ActiveRecord::Base.connection.
+ execute("UPDATE containers "+
+ "SET state='#{Locked}', auth_uuid='#{auth_uuid}', "+
+ "locked_by_uuid='#{locked_by_uuid}', modified_at='#{now}' "+
+ "WHERE uuid='#{uuid}' AND state='#{Queued}' and priority>0")
+ if r.cmd_status != 'UPDATE 1'
+ if r.cmd_status != 'UPDATE 0'
+ Rails.logger.error("unexpected db result: #{r.cmd_status.inspect}")
+ end
+ raise LockFailedError.new("cannot lock: lost race with another caller")
end
- self.state = Locked
- self.save!
+ c["updated_at"] = now
+ c["modified_at"] = now
+ c["modified_by_user_uuid"] = current_user.uuid
+ c["auth_uuid"] = auth_uuid
+ c["locked_by_uuid"] = locked_by_uuid
+ c["state"] = Locked
+ c.send(:log_update)
end
end
def unlock
+ if self.state != Locked
+ raise InvalidStateTransitionError
+ end
with_lock do
- if self.state == Queued
+ if self.state != Locked
raise InvalidStateTransitionError
end
- self.state = Queued
- self.save!
+ self.update_attributes!(state: Queued)
end
end
diff --git a/services/api/test/functional/arvados/v1/containers_controller_test.rb b/services/api/test/functional/arvados/v1/containers_controller_test.rb
index 65a1a91..5aa7ea7 100644
--- a/services/api/test/functional/arvados/v1/containers_controller_test.rb
+++ b/services/api/test/functional/arvados/v1/containers_controller_test.rb
@@ -24,7 +24,7 @@ class Arvados::V1::ContainersControllerTest < ActionController::TestCase
test 'cannot get auth with wrong token' do
authorize_with :dispatch1
c = containers(:queued)
- assert c.lock, show_errors(c)
+ assert c.lock_and_reload, show_errors(c)
authorize_with :system_user
get :auth, id: c.uuid
@@ -34,7 +34,7 @@ class Arvados::V1::ContainersControllerTest < ActionController::TestCase
test 'get auth' do
authorize_with :dispatch1
c = containers(:queued)
- assert c.lock, show_errors(c)
+ assert c.lock_and_reload, show_errors(c)
get :auth, id: c.uuid
assert_response :success
assert_operator 32, :<, json_response['api_token'].length
@@ -44,7 +44,7 @@ class Arvados::V1::ContainersControllerTest < ActionController::TestCase
test 'no auth in container response' do
authorize_with :dispatch1
c = containers(:queued)
- assert c.lock, show_errors(c)
+ assert c.lock_and_reload, show_errors(c)
get :show, id: c.uuid
assert_response :success
assert_nil json_response['auth']
diff --git a/services/api/test/unit/container_test.rb b/services/api/test/unit/container_test.rb
index 52d2aa6..c9fbf11 100644
--- a/services/api/test/unit/container_test.rb
+++ b/services/api/test/unit/container_test.rb
@@ -347,7 +347,7 @@ class ContainerTest < ActiveSupport::TestCase
check_illegal_updates c, [{state: Container::Running},
{state: Container::Complete}]
- c.lock
+ c.lock_and_reload
c.update_attributes! state: Container::Running
check_illegal_modify c
@@ -365,7 +365,7 @@ class ContainerTest < ActiveSupport::TestCase
set_user_from_auth :dispatch1
assert_equal Container::Queued, c.state
- assert_raise(ActiveRecord::RecordInvalid) {c.lock} # "no priority"
+ assert_raise(ActiveRecord::RecordInvalid) {c.lock_and_reload} # "no priority"
c.reload
assert cr.update_attributes priority: 1
@@ -374,11 +374,11 @@ class ContainerTest < ActiveSupport::TestCase
refute c.update_attributes(state: Container::Complete), "not locked"
c.reload
- assert c.lock, show_errors(c)
+ assert c.lock_and_reload, show_errors(c)
assert c.locked_by_uuid
assert c.auth_uuid
- assert_raise(ArvadosModel::AlreadyLockedError) {c.lock}
+ assert_raise(ArvadosModel::AlreadyLockedError) {c.lock_and_reload}
c.reload
assert c.unlock, show_errors(c)
@@ -390,14 +390,14 @@ class ContainerTest < ActiveSupport::TestCase
refute c.locked_by_uuid
refute c.auth_uuid
- assert c.lock, show_errors(c)
+ assert c.lock_and_reload, show_errors(c)
assert c.update_attributes(state: Container::Running), show_errors(c)
assert c.locked_by_uuid
assert c.auth_uuid
auth_uuid_was = c.auth_uuid
- assert_raise(ActiveRecord::RecordInvalid) {c.lock} # Running to Locked is not allowed
+ assert_raise(ActiveRecord::RecordInvalid) {c.lock_and_reload} # Running to Locked is not allowed
c.reload
assert_raise(ActiveRecord::RecordInvalid) {c.unlock} # Running to Queued is not allowed
c.reload
@@ -420,7 +420,7 @@ class ContainerTest < ActiveSupport::TestCase
test "Container locked cancel" do
c, _ = minimal_new
set_user_from_auth :dispatch1
- assert c.lock, show_errors(c)
+ assert c.lock_and_reload, show_errors(c)
assert c.update_attributes(state: Container::Cancelled), show_errors(c)
check_no_change_from_cancelled c
end
@@ -428,7 +428,7 @@ class ContainerTest < ActiveSupport::TestCase
test "Container running cancel" do
c, _ = minimal_new
set_user_from_auth :dispatch1
- c.lock
+ c.lock_and_reload
c.update_attributes! state: Container::Running
c.update_attributes! state: Container::Cancelled
check_no_change_from_cancelled c
@@ -450,7 +450,7 @@ class ContainerTest < ActiveSupport::TestCase
test "Container only set exit code on complete" do
c, _ = minimal_new
set_user_from_auth :dispatch1
- c.lock
+ c.lock_and_reload
c.update_attributes! state: Container::Running
check_illegal_updates c, [{exit_code: 1},
@@ -462,7 +462,7 @@ class ContainerTest < ActiveSupport::TestCase
test "locked_by_uuid can set output on running container" do
c, _ = minimal_new
set_user_from_auth :dispatch1
- c.lock
+ c.lock_and_reload
c.update_attributes! state: Container::Running
assert_equal c.locked_by_uuid, Thread.current[:api_client_authorization].uuid
@@ -474,7 +474,7 @@ class ContainerTest < ActiveSupport::TestCase
test "auth_uuid can set output on running container, but not change container state" do
c, _ = minimal_new
set_user_from_auth :dispatch1
- c.lock
+ c.lock_and_reload
c.update_attributes! state: Container::Running
Thread.current[:api_client_authorization] = ApiClientAuthorization.find_by_uuid(c.auth_uuid)
@@ -490,7 +490,7 @@ class ContainerTest < ActiveSupport::TestCase
test "not allowed to set output that is not readable by current user" do
c, _ = minimal_new
set_user_from_auth :dispatch1
- c.lock
+ c.lock_and_reload
c.update_attributes! state: Container::Running
Thread.current[:api_client_authorization] = ApiClientAuthorization.find_by_uuid(c.auth_uuid)
@@ -504,7 +504,7 @@ class ContainerTest < ActiveSupport::TestCase
test "other token cannot set output on running container" do
c, _ = minimal_new
set_user_from_auth :dispatch1
- c.lock
+ c.lock_and_reload
c.update_attributes! state: Container::Running
set_user_from_auth :not_running_container_auth
@@ -516,7 +516,7 @@ class ContainerTest < ActiveSupport::TestCase
test "can set trashed output on running container" do
c, _ = minimal_new
set_user_from_auth :dispatch1
- c.lock
+ c.lock_and_reload
c.update_attributes! state: Container::Running
output = Collection.unscoped.find_by_uuid('zzzzz-4zz18-mto52zx1s7sn3jk')
@@ -529,7 +529,7 @@ class ContainerTest < ActiveSupport::TestCase
test "not allowed to set trashed output that is not readable by current user" do
c, _ = minimal_new
set_user_from_auth :dispatch1
- c.lock
+ c.lock_and_reload
c.update_attributes! state: Container::Running
output = Collection.unscoped.find_by_uuid('zzzzz-4zz18-mto52zx1s7sn3jr')
-----------------------------------------------------------------------
hooks/post-receive
--
More information about the arvados-commits
mailing list