From 9ee717744ac8bf94f10370e29757a892c0cf08a7 Mon Sep 17 00:00:00 2001 From: Kirill Solomko Date: Thu, 10 Jul 2025 13:04:36 +0200 Subject: [PATCH] MT#63199 /api/subscriberpreferences add lockwait and deadlock detection * lockwait and deadlock detection is now in place for ALL /api/subscriberpreferences endpoint methods. Change-Id: Icb613677dfca1187b3a1e707588ad6e9547d3c9a --- .../Controller/API/SubscriberPreferences.pm | 108 ++++---- .../API/SubscriberPreferencesItem.pm | 248 ++++++++++-------- 2 files changed, 202 insertions(+), 154 deletions(-) diff --git a/lib/NGCP/Panel/Controller/API/SubscriberPreferences.pm b/lib/NGCP/Panel/Controller/API/SubscriberPreferences.pm index 6058bca360..c5de86e164 100644 --- a/lib/NGCP/Panel/Controller/API/SubscriberPreferences.pm +++ b/lib/NGCP/Panel/Controller/API/SubscriberPreferences.pm @@ -88,56 +88,68 @@ sub GET :Allow { my $page = $c->request->params->{page} // 1; my $rows = $c->request->params->{rows} // 10; $c->model('DB')->set_transaction_isolation('READ COMMITTED'); - my $guard = $c->model('DB')->txn_scope_guard; - { - my $subscribers_rs = $self->item_rs($c, "subscribers"); - (my $total_count, $subscribers_rs, my $subscribers_rows) = $self->paginate_order_collection($c, $subscribers_rs); - my $subscribers = NGCP::Panel::Utils::Contract::acquire_contract_rowlocks( - c => $c, - rs => $subscribers_rs, - contract_id_field => 'contract_id', - skip_locked => ($c->request->header('X-Delay-Commit') ? 0 : 1), - ); - my $now = NGCP::Panel::Utils::DateTime::current_local; - my (@embedded, @links, %contract_map); - $self->expand_prepare_collection($c); - for my $subscriber (@$subscribers) { - next unless($subscriber->provisioning_voip_subscriber); - my $contract = $subscriber->contract; - my $balance; $balance = NGCP::Panel::Utils::ProfilePackages::get_contract_balance(c => $c, - contract => $contract, - now => $now) if !exists $contract_map{$contract->id}; #apply underrun lock level - $contract_map{$contract->id} = 1; - push @embedded, $self->hal_from_item($c, $subscriber, "subscribers"); - push @links, Data::HAL::Link->new( - relation => 'ngcp:'.$self->resource_name, - href => sprintf('%s%d', $self->dispatch_path, $subscriber->id), + TX_START: + $c->clear_errors; + try { + my $guard = $c->model('DB')->txn_scope_guard; + { + my $subscribers_rs = $self->item_rs($c, "subscribers"); + (my $total_count, $subscribers_rs, my $subscribers_rows) = $self->paginate_order_collection($c, $subscribers_rs); + my $subscribers = NGCP::Panel::Utils::Contract::acquire_contract_rowlocks( + c => $c, + rs => $subscribers_rs, + contract_id_field => 'contract_id', + skip_locked => ($c->request->header('X-Delay-Commit') ? 0 : 1), + ); + my $now = NGCP::Panel::Utils::DateTime::current_local; + my (@embedded, @links, %contract_map); + $self->expand_prepare_collection($c); + for my $subscriber (@$subscribers) { + next unless($subscriber->provisioning_voip_subscriber); + my $contract = $subscriber->contract; + my $balance; $balance = NGCP::Panel::Utils::ProfilePackages::get_contract_balance(c => $c, + contract => $contract, + now => $now) if !exists $contract_map{$contract->id}; #apply underrun lock level + $contract_map{$contract->id} = 1; + push @embedded, $self->hal_from_item($c, $subscriber, "subscribers"); + push @links, Data::HAL::Link->new( + relation => 'ngcp:'.$self->resource_name, + href => sprintf('%s%d', $self->dispatch_path, $subscriber->id), + ); + } + $self->expand_collection_fields($c, \@embedded); + $self->delay_commit($c,$guard); + push @links, + Data::HAL::Link->new( + relation => 'curies', + href => 'http://purl.org/sipwise/ngcp-api/#rel-{rel}', + name => 'ngcp', + templated => true, + ), + Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'), + $self->collection_nav_links($c, $page, $rows, $total_count, $c->request->path, $c->request->query_params); + + my $hal = Data::HAL->new( + embedded => [@embedded], + links => [@links], ); + $hal->resource({ + total_count => $total_count, + }); + my $response = HTTP::Response->new(HTTP_OK, undef, + HTTP::Headers->new($hal->http_headers(skip_links => 1)), $hal->as_json); + $c->response->headers($response->headers); + $c->response->body($response->content); + return; + } + } catch($e) { + if ($self->check_deadlock($c, $e)) { + goto TX_START; + } + unless ($c->has_errors) { + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, 'Internal Server Error', $e); + last; } - $self->expand_collection_fields($c, \@embedded); - $self->delay_commit($c,$guard); - push @links, - Data::HAL::Link->new( - relation => 'curies', - href => 'http://purl.org/sipwise/ngcp-api/#rel-{rel}', - name => 'ngcp', - templated => true, - ), - Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'), - $self->collection_nav_links($c, $page, $rows, $total_count, $c->request->path, $c->request->query_params); - - my $hal = Data::HAL->new( - embedded => [@embedded], - links => [@links], - ); - $hal->resource({ - total_count => $total_count, - }); - my $response = HTTP::Response->new(HTTP_OK, undef, - HTTP::Headers->new($hal->http_headers(skip_links => 1)), $hal->as_json); - $c->response->headers($response->headers); - $c->response->body($response->content); - return; } return; } diff --git a/lib/NGCP/Panel/Controller/API/SubscriberPreferencesItem.pm b/lib/NGCP/Panel/Controller/API/SubscriberPreferencesItem.pm index cbcf5e1e4e..127d18b572 100644 --- a/lib/NGCP/Panel/Controller/API/SubscriberPreferencesItem.pm +++ b/lib/NGCP/Panel/Controller/API/SubscriberPreferencesItem.pm @@ -50,27 +50,39 @@ sub journal_query_params { sub GET :Allow { my ($self, $c, $id) = @_; $c->model('DB')->set_transaction_isolation('READ COMMITTED'); - my $guard = $c->model('DB')->txn_scope_guard; - { - last unless $self->valid_id($c, $id); - my $subscriber = $self->item_by_id($c, $id, "subscribers"); - last unless $self->resource_exists($c, subscriberpreference => $subscriber); - - my $balance = NGCP::Panel::Utils::ProfilePackages::get_contract_balance(c => $c, - contract => $subscriber->contract, - ); #apply underrun lock level - my $hal = $self->hal_from_item($c, $subscriber, "subscribers"); - $guard->commit; #potential db write ops in hal_from - - my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( - (map { # XXX Data::HAL must be able to generate links with multiple relations - s|rel="(http://purl.org/sipwise/ngcp-api/#rel-resellers)"|rel="item $1"|r - =~ s/rel=self/rel="item self"/r; - } $hal->http_headers), - ), $hal->as_json); - $c->response->headers($response->headers); - $c->response->body($response->content); - return; + TX_START: + $c->clear_errors; + try { + my $guard = $c->model('DB')->txn_scope_guard; + { + last unless $self->valid_id($c, $id); + my $subscriber = $self->item_by_id($c, $id, "subscribers"); + last unless $self->resource_exists($c, subscriberpreference => $subscriber); + + my $balance = NGCP::Panel::Utils::ProfilePackages::get_contract_balance(c => $c, + contract => $subscriber->contract, + ); #apply underrun lock level + my $hal = $self->hal_from_item($c, $subscriber, "subscribers"); + $guard->commit; #potential db write ops in hal_from + + my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( + (map { # XXX Data::HAL must be able to generate links with multiple relations + s|rel="(http://purl.org/sipwise/ngcp-api/#rel-resellers)"|rel="item $1"|r + =~ s/rel=self/rel="item self"/r; + } $hal->http_headers), + ), $hal->as_json); + $c->response->headers($response->headers); + $c->response->body($response->content); + return; + } + } catch($e) { + if ($self->check_deadlock($c, $e)) { + goto TX_START; + } + unless ($c->has_errors) { + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, 'Internal Server Error', $e); + last; + } } return; } @@ -78,51 +90,63 @@ sub GET :Allow { sub PATCH :Allow { my ($self, $c, $id) = @_; $c->model('DB')->set_transaction_isolation('READ COMMITTED'); - my $guard = $c->model('DB')->txn_scope_guard; - { - my $preference = $self->require_preference($c); - last unless $preference; - - my $json = $self->get_valid_patch_data( - c => $c, - id => $id, - media_type => 'application/json-patch+json', - ops => [qw/add replace remove copy/], - ); - last unless $json; - - my $subscriber = $self->item_by_id($c, $id, "subscribers"); - last unless $self->resource_exists($c, subscriberpreferences => $subscriber); - my $balance = NGCP::Panel::Utils::ProfilePackages::get_contract_balance(c => $c, - contract => $subscriber->contract, - ); #apply underrun lock level - - my $old_resource = $self->get_resource($c, $subscriber, "subscribers"); - my $resource = $self->apply_patch($c, $old_resource, $json); - last unless $resource; - - # last param is "no replace" to NOT delete existing prefs - # for proper PATCH behavior - $subscriber = $self->update_item($c, $subscriber, $old_resource, $resource, 0, "subscribers"); - last unless $subscriber; - - my $hal = $self->hal_from_item($c, $subscriber, "subscribers"); - last unless $self->add_update_journal_item_hal($c,$hal); - - $guard->commit; - - if ('minimal' eq $preference) { - $c->response->status(HTTP_NO_CONTENT); - $c->response->header(Preference_Applied => 'return=minimal'); - $c->response->body(q()); - } else { - #my $hal = $self->hal_from_item($c, $subscriber, "subscribers"); - my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( - $hal->http_headers, - ), $hal->as_json); - $c->response->headers($response->headers); - $c->response->header(Preference_Applied => 'return=representation'); - $c->response->body($response->content); + TX_START: + $c->clear_errors; + try { + my $guard = $c->model('DB')->txn_scope_guard; + { + my $preference = $self->require_preference($c); + last unless $preference; + + my $json = $self->get_valid_patch_data( + c => $c, + id => $id, + media_type => 'application/json-patch+json', + ops => [qw/add replace remove copy/], + ); + last unless $json; + + my $subscriber = $self->item_by_id($c, $id, "subscribers"); + last unless $self->resource_exists($c, subscriberpreferences => $subscriber); + my $balance = NGCP::Panel::Utils::ProfilePackages::get_contract_balance(c => $c, + contract => $subscriber->contract, + ); #apply underrun lock level + + my $old_resource = $self->get_resource($c, $subscriber, "subscribers"); + my $resource = $self->apply_patch($c, $old_resource, $json); + last unless $resource; + + # last param is "no replace" to NOT delete existing prefs + # for proper PATCH behavior + $subscriber = $self->update_item($c, $subscriber, $old_resource, $resource, 0, "subscribers"); + last unless $subscriber; + + my $hal = $self->hal_from_item($c, $subscriber, "subscribers"); + last unless $self->add_update_journal_item_hal($c,$hal); + + $guard->commit; + + if ('minimal' eq $preference) { + $c->response->status(HTTP_NO_CONTENT); + $c->response->header(Preference_Applied => 'return=minimal'); + $c->response->body(q()); + } else { + #my $hal = $self->hal_from_item($c, $subscriber, "subscribers"); + my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( + $hal->http_headers, + ), $hal->as_json); + $c->response->headers($response->headers); + $c->response->header(Preference_Applied => 'return=representation'); + $c->response->body($response->content); + } + } + } catch($e) { + if ($self->check_deadlock($c, $e)) { + goto TX_START; + } + unless ($c->has_errors) { + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, 'Internal Server Error', $e); + last; } } return; @@ -131,46 +155,58 @@ sub PATCH :Allow { sub PUT :Allow { my ($self, $c, $id) = @_; $c->model('DB')->set_transaction_isolation('READ COMMITTED'); - my $guard = $c->model('DB')->txn_scope_guard; - { - my $preference = $self->require_preference($c); - last unless $preference; - - my $subscriber = $self->item_by_id($c, $id, "subscribers"); - last unless $self->resource_exists($c, systemcontact => $subscriber); - my $balance = NGCP::Panel::Utils::ProfilePackages::get_contract_balance(c => $c, - contract => $subscriber->contract, - ); #apply underrun lock level - my $resource = $self->get_valid_put_data( - c => $c, - id => $id, - media_type => 'application/json', - ); - last unless $resource; - my $old_resource = $self->get_resource($c, $subscriber, "subscribers"); - - # last param is "replace" to delete all existing prefs - # for proper PUT behavior - $subscriber = $self->update_item($c, $subscriber, $old_resource, $resource, 1, "subscribers"); - last unless $subscriber; - - my $hal = $self->hal_from_item($c, $subscriber, "subscribers"); - last unless $self->add_update_journal_item_hal($c,$hal); - - $guard->commit; - - if ('minimal' eq $preference) { - $c->response->status(HTTP_NO_CONTENT); - $c->response->header(Preference_Applied => 'return=minimal'); - $c->response->body(q()); - } else { - #my $hal = $self->hal_from_item($c, $subscriber, "subscribers"); - my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( - $hal->http_headers, - ), $hal->as_json); - $c->response->headers($response->headers); - $c->response->header(Preference_Applied => 'return=representation'); - $c->response->body($response->content); + TX_START: + $c->clear_errors; + try { + my $guard = $c->model('DB')->txn_scope_guard; + { + my $preference = $self->require_preference($c); + last unless $preference; + + my $subscriber = $self->item_by_id($c, $id, "subscribers"); + last unless $self->resource_exists($c, systemcontact => $subscriber); + my $balance = NGCP::Panel::Utils::ProfilePackages::get_contract_balance(c => $c, + contract => $subscriber->contract, + ); #apply underrun lock level + my $resource = $self->get_valid_put_data( + c => $c, + id => $id, + media_type => 'application/json', + ); + last unless $resource; + my $old_resource = $self->get_resource($c, $subscriber, "subscribers"); + + # last param is "replace" to delete all existing prefs + # for proper PUT behavior + $subscriber = $self->update_item($c, $subscriber, $old_resource, $resource, 1, "subscribers"); + last unless $subscriber; + + my $hal = $self->hal_from_item($c, $subscriber, "subscribers"); + last unless $self->add_update_journal_item_hal($c,$hal); + + $guard->commit; + + if ('minimal' eq $preference) { + $c->response->status(HTTP_NO_CONTENT); + $c->response->header(Preference_Applied => 'return=minimal'); + $c->response->body(q()); + } else { + #my $hal = $self->hal_from_item($c, $subscriber, "subscribers"); + my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( + $hal->http_headers, + ), $hal->as_json); + $c->response->headers($response->headers); + $c->response->header(Preference_Applied => 'return=representation'); + $c->response->body($response->content); + } + } + } catch($e) { + if ($self->check_deadlock($c, $e)) { + goto TX_START; + } + unless ($c->has_errors) { + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, 'Internal Server Error', $e); + last; } } return;