rucio 37.0.0rc1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of rucio might be problematic. Click here for more details.
- rucio/__init__.py +17 -0
- rucio/alembicrevision.py +15 -0
- rucio/cli/__init__.py +14 -0
- rucio/cli/account.py +216 -0
- rucio/cli/bin_legacy/__init__.py +13 -0
- rucio/cli/bin_legacy/rucio.py +2825 -0
- rucio/cli/bin_legacy/rucio_admin.py +2500 -0
- rucio/cli/command.py +272 -0
- rucio/cli/config.py +72 -0
- rucio/cli/did.py +191 -0
- rucio/cli/download.py +128 -0
- rucio/cli/lifetime_exception.py +33 -0
- rucio/cli/replica.py +162 -0
- rucio/cli/rse.py +293 -0
- rucio/cli/rule.py +158 -0
- rucio/cli/scope.py +40 -0
- rucio/cli/subscription.py +73 -0
- rucio/cli/upload.py +60 -0
- rucio/cli/utils.py +226 -0
- rucio/client/__init__.py +15 -0
- rucio/client/accountclient.py +432 -0
- rucio/client/accountlimitclient.py +183 -0
- rucio/client/baseclient.py +983 -0
- rucio/client/client.py +120 -0
- rucio/client/configclient.py +126 -0
- rucio/client/credentialclient.py +59 -0
- rucio/client/didclient.py +868 -0
- rucio/client/diracclient.py +56 -0
- rucio/client/downloadclient.py +1783 -0
- rucio/client/exportclient.py +44 -0
- rucio/client/fileclient.py +50 -0
- rucio/client/importclient.py +42 -0
- rucio/client/lifetimeclient.py +90 -0
- rucio/client/lockclient.py +109 -0
- rucio/client/metaconventionsclient.py +140 -0
- rucio/client/pingclient.py +44 -0
- rucio/client/replicaclient.py +452 -0
- rucio/client/requestclient.py +125 -0
- rucio/client/richclient.py +317 -0
- rucio/client/rseclient.py +746 -0
- rucio/client/ruleclient.py +294 -0
- rucio/client/scopeclient.py +90 -0
- rucio/client/subscriptionclient.py +173 -0
- rucio/client/touchclient.py +82 -0
- rucio/client/uploadclient.py +969 -0
- rucio/common/__init__.py +13 -0
- rucio/common/bittorrent.py +234 -0
- rucio/common/cache.py +111 -0
- rucio/common/checksum.py +168 -0
- rucio/common/client.py +122 -0
- rucio/common/config.py +788 -0
- rucio/common/constants.py +217 -0
- rucio/common/constraints.py +17 -0
- rucio/common/didtype.py +237 -0
- rucio/common/dumper/__init__.py +342 -0
- rucio/common/dumper/consistency.py +497 -0
- rucio/common/dumper/data_models.py +362 -0
- rucio/common/dumper/path_parsing.py +75 -0
- rucio/common/exception.py +1208 -0
- rucio/common/extra.py +31 -0
- rucio/common/logging.py +420 -0
- rucio/common/pcache.py +1409 -0
- rucio/common/plugins.py +185 -0
- rucio/common/policy.py +93 -0
- rucio/common/schema/__init__.py +200 -0
- rucio/common/schema/generic.py +416 -0
- rucio/common/schema/generic_multi_vo.py +395 -0
- rucio/common/stomp_utils.py +423 -0
- rucio/common/stopwatch.py +55 -0
- rucio/common/test_rucio_server.py +154 -0
- rucio/common/types.py +483 -0
- rucio/common/utils.py +1688 -0
- rucio/core/__init__.py +13 -0
- rucio/core/account.py +496 -0
- rucio/core/account_counter.py +236 -0
- rucio/core/account_limit.py +425 -0
- rucio/core/authentication.py +620 -0
- rucio/core/config.py +437 -0
- rucio/core/credential.py +224 -0
- rucio/core/did.py +3004 -0
- rucio/core/did_meta_plugins/__init__.py +252 -0
- rucio/core/did_meta_plugins/did_column_meta.py +331 -0
- rucio/core/did_meta_plugins/did_meta_plugin_interface.py +165 -0
- rucio/core/did_meta_plugins/elasticsearch_meta.py +407 -0
- rucio/core/did_meta_plugins/filter_engine.py +672 -0
- rucio/core/did_meta_plugins/json_meta.py +240 -0
- rucio/core/did_meta_plugins/mongo_meta.py +229 -0
- rucio/core/did_meta_plugins/postgres_meta.py +352 -0
- rucio/core/dirac.py +237 -0
- rucio/core/distance.py +187 -0
- rucio/core/exporter.py +59 -0
- rucio/core/heartbeat.py +363 -0
- rucio/core/identity.py +301 -0
- rucio/core/importer.py +260 -0
- rucio/core/lifetime_exception.py +377 -0
- rucio/core/lock.py +577 -0
- rucio/core/message.py +288 -0
- rucio/core/meta_conventions.py +203 -0
- rucio/core/monitor.py +448 -0
- rucio/core/naming_convention.py +195 -0
- rucio/core/nongrid_trace.py +136 -0
- rucio/core/oidc.py +1463 -0
- rucio/core/permission/__init__.py +161 -0
- rucio/core/permission/generic.py +1124 -0
- rucio/core/permission/generic_multi_vo.py +1144 -0
- rucio/core/quarantined_replica.py +224 -0
- rucio/core/replica.py +4483 -0
- rucio/core/replica_sorter.py +362 -0
- rucio/core/request.py +3091 -0
- rucio/core/rse.py +2079 -0
- rucio/core/rse_counter.py +185 -0
- rucio/core/rse_expression_parser.py +459 -0
- rucio/core/rse_selector.py +304 -0
- rucio/core/rule.py +4484 -0
- rucio/core/rule_grouping.py +1620 -0
- rucio/core/scope.py +181 -0
- rucio/core/subscription.py +362 -0
- rucio/core/topology.py +490 -0
- rucio/core/trace.py +375 -0
- rucio/core/transfer.py +1531 -0
- rucio/core/vo.py +169 -0
- rucio/core/volatile_replica.py +151 -0
- rucio/daemons/__init__.py +13 -0
- rucio/daemons/abacus/__init__.py +13 -0
- rucio/daemons/abacus/account.py +116 -0
- rucio/daemons/abacus/collection_replica.py +124 -0
- rucio/daemons/abacus/rse.py +117 -0
- rucio/daemons/atropos/__init__.py +13 -0
- rucio/daemons/atropos/atropos.py +242 -0
- rucio/daemons/auditor/__init__.py +289 -0
- rucio/daemons/auditor/hdfs.py +97 -0
- rucio/daemons/auditor/srmdumps.py +355 -0
- rucio/daemons/automatix/__init__.py +13 -0
- rucio/daemons/automatix/automatix.py +304 -0
- rucio/daemons/badreplicas/__init__.py +13 -0
- rucio/daemons/badreplicas/minos.py +322 -0
- rucio/daemons/badreplicas/minos_temporary_expiration.py +171 -0
- rucio/daemons/badreplicas/necromancer.py +196 -0
- rucio/daemons/bb8/__init__.py +13 -0
- rucio/daemons/bb8/bb8.py +353 -0
- rucio/daemons/bb8/common.py +759 -0
- rucio/daemons/bb8/nuclei_background_rebalance.py +153 -0
- rucio/daemons/bb8/t2_background_rebalance.py +153 -0
- rucio/daemons/cache/__init__.py +13 -0
- rucio/daemons/cache/consumer.py +133 -0
- rucio/daemons/common.py +405 -0
- rucio/daemons/conveyor/__init__.py +13 -0
- rucio/daemons/conveyor/common.py +562 -0
- rucio/daemons/conveyor/finisher.py +529 -0
- rucio/daemons/conveyor/poller.py +394 -0
- rucio/daemons/conveyor/preparer.py +205 -0
- rucio/daemons/conveyor/receiver.py +179 -0
- rucio/daemons/conveyor/stager.py +133 -0
- rucio/daemons/conveyor/submitter.py +403 -0
- rucio/daemons/conveyor/throttler.py +532 -0
- rucio/daemons/follower/__init__.py +13 -0
- rucio/daemons/follower/follower.py +101 -0
- rucio/daemons/hermes/__init__.py +13 -0
- rucio/daemons/hermes/hermes.py +534 -0
- rucio/daemons/judge/__init__.py +13 -0
- rucio/daemons/judge/cleaner.py +159 -0
- rucio/daemons/judge/evaluator.py +185 -0
- rucio/daemons/judge/injector.py +162 -0
- rucio/daemons/judge/repairer.py +154 -0
- rucio/daemons/oauthmanager/__init__.py +13 -0
- rucio/daemons/oauthmanager/oauthmanager.py +198 -0
- rucio/daemons/reaper/__init__.py +13 -0
- rucio/daemons/reaper/dark_reaper.py +282 -0
- rucio/daemons/reaper/reaper.py +739 -0
- rucio/daemons/replicarecoverer/__init__.py +13 -0
- rucio/daemons/replicarecoverer/suspicious_replica_recoverer.py +626 -0
- rucio/daemons/rsedecommissioner/__init__.py +13 -0
- rucio/daemons/rsedecommissioner/config.py +81 -0
- rucio/daemons/rsedecommissioner/profiles/__init__.py +24 -0
- rucio/daemons/rsedecommissioner/profiles/atlas.py +60 -0
- rucio/daemons/rsedecommissioner/profiles/generic.py +452 -0
- rucio/daemons/rsedecommissioner/profiles/types.py +93 -0
- rucio/daemons/rsedecommissioner/rse_decommissioner.py +280 -0
- rucio/daemons/storage/__init__.py +13 -0
- rucio/daemons/storage/consistency/__init__.py +13 -0
- rucio/daemons/storage/consistency/actions.py +848 -0
- rucio/daemons/tracer/__init__.py +13 -0
- rucio/daemons/tracer/kronos.py +511 -0
- rucio/daemons/transmogrifier/__init__.py +13 -0
- rucio/daemons/transmogrifier/transmogrifier.py +762 -0
- rucio/daemons/undertaker/__init__.py +13 -0
- rucio/daemons/undertaker/undertaker.py +137 -0
- rucio/db/__init__.py +13 -0
- rucio/db/sqla/__init__.py +52 -0
- rucio/db/sqla/constants.py +206 -0
- rucio/db/sqla/migrate_repo/__init__.py +13 -0
- rucio/db/sqla/migrate_repo/env.py +110 -0
- rucio/db/sqla/migrate_repo/versions/01eaf73ab656_add_new_rule_notification_state_progress.py +70 -0
- rucio/db/sqla/migrate_repo/versions/0437a40dbfd1_add_eol_at_in_rules.py +47 -0
- rucio/db/sqla/migrate_repo/versions/0f1adb7a599a_create_transfer_hops_table.py +59 -0
- rucio/db/sqla/migrate_repo/versions/102efcf145f4_added_stuck_at_column_to_rules.py +43 -0
- rucio/db/sqla/migrate_repo/versions/13d4f70c66a9_introduce_transfer_limits.py +91 -0
- rucio/db/sqla/migrate_repo/versions/140fef722e91_cleanup_distances_table.py +76 -0
- rucio/db/sqla/migrate_repo/versions/14ec5aeb64cf_add_request_external_host.py +43 -0
- rucio/db/sqla/migrate_repo/versions/156fb5b5a14_add_request_type_to_requests_idx.py +50 -0
- rucio/db/sqla/migrate_repo/versions/1677d4d803c8_split_rse_availability_into_multiple.py +68 -0
- rucio/db/sqla/migrate_repo/versions/16a0aca82e12_create_index_on_table_replicas_path.py +40 -0
- rucio/db/sqla/migrate_repo/versions/1803333ac20f_adding_provenance_and_phys_group.py +45 -0
- rucio/db/sqla/migrate_repo/versions/1a29d6a9504c_add_didtype_chck_to_requests.py +60 -0
- rucio/db/sqla/migrate_repo/versions/1a80adff031a_create_index_on_rules_hist_recent.py +40 -0
- rucio/db/sqla/migrate_repo/versions/1c45d9730ca6_increase_identity_length.py +140 -0
- rucio/db/sqla/migrate_repo/versions/1d1215494e95_add_quarantined_replicas_table.py +73 -0
- rucio/db/sqla/migrate_repo/versions/1d96f484df21_asynchronous_rules_and_rule_approval.py +74 -0
- rucio/db/sqla/migrate_repo/versions/1f46c5f240ac_add_bytes_column_to_bad_replicas.py +43 -0
- rucio/db/sqla/migrate_repo/versions/1fc15ab60d43_add_message_history_table.py +50 -0
- rucio/db/sqla/migrate_repo/versions/2190e703eb6e_move_rse_settings_to_rse_attributes.py +134 -0
- rucio/db/sqla/migrate_repo/versions/21d6b9dc9961_add_mismatch_scheme_state_to_requests.py +64 -0
- rucio/db/sqla/migrate_repo/versions/22cf51430c78_add_availability_column_to_table_rses.py +39 -0
- rucio/db/sqla/migrate_repo/versions/22d887e4ec0a_create_sources_table.py +64 -0
- rucio/db/sqla/migrate_repo/versions/25821a8a45a3_remove_unique_constraint_on_requests.py +51 -0
- rucio/db/sqla/migrate_repo/versions/25fc855625cf_added_unique_constraint_to_rules.py +41 -0
- rucio/db/sqla/migrate_repo/versions/269fee20dee9_add_repair_cnt_to_locks.py +43 -0
- rucio/db/sqla/migrate_repo/versions/271a46ea6244_add_ignore_availability_column_to_rules.py +44 -0
- rucio/db/sqla/migrate_repo/versions/277b5fbb41d3_switch_heartbeats_executable.py +53 -0
- rucio/db/sqla/migrate_repo/versions/27e3a68927fb_remove_replicas_tombstone_and_replicas_.py +38 -0
- rucio/db/sqla/migrate_repo/versions/2854cd9e168_added_rule_id_column.py +47 -0
- rucio/db/sqla/migrate_repo/versions/295289b5a800_processed_by_and__at_in_requests.py +45 -0
- rucio/db/sqla/migrate_repo/versions/2962ece31cf4_add_nbaccesses_column_in_the_did_table.py +45 -0
- rucio/db/sqla/migrate_repo/versions/2af3291ec4c_added_replicas_history_table.py +57 -0
- rucio/db/sqla/migrate_repo/versions/2b69addda658_add_columns_for_third_party_copy_read_.py +45 -0
- rucio/db/sqla/migrate_repo/versions/2b8e7bcb4783_add_config_table.py +69 -0
- rucio/db/sqla/migrate_repo/versions/2ba5229cb54c_add_submitted_at_to_requests_table.py +43 -0
- rucio/db/sqla/migrate_repo/versions/2cbee484dcf9_added_column_volume_to_rse_transfer_.py +42 -0
- rucio/db/sqla/migrate_repo/versions/2edee4a83846_add_source_to_requests_and_requests_.py +47 -0
- rucio/db/sqla/migrate_repo/versions/2eef46be23d4_change_tokens_pk.py +46 -0
- rucio/db/sqla/migrate_repo/versions/2f648fc909f3_index_in_rule_history_on_scope_name.py +40 -0
- rucio/db/sqla/migrate_repo/versions/3082b8cef557_add_naming_convention_table_and_closed_.py +67 -0
- rucio/db/sqla/migrate_repo/versions/30d5206e9cad_increase_oauthrequest_redirect_msg_.py +37 -0
- rucio/db/sqla/migrate_repo/versions/30fa38b6434e_add_index_on_service_column_in_the_message_table.py +44 -0
- rucio/db/sqla/migrate_repo/versions/3152492b110b_added_staging_area_column.py +77 -0
- rucio/db/sqla/migrate_repo/versions/32c7d2783f7e_create_bad_replicas_table.py +60 -0
- rucio/db/sqla/migrate_repo/versions/3345511706b8_replicas_table_pk_definition_is_in_.py +72 -0
- rucio/db/sqla/migrate_repo/versions/35ef10d1e11b_change_index_on_table_requests.py +42 -0
- rucio/db/sqla/migrate_repo/versions/379a19b5332d_create_rse_limits_table.py +65 -0
- rucio/db/sqla/migrate_repo/versions/384b96aa0f60_created_rule_history_tables.py +133 -0
- rucio/db/sqla/migrate_repo/versions/3ac1660a1a72_extend_distance_table.py +55 -0
- rucio/db/sqla/migrate_repo/versions/3ad36e2268b0_create_collection_replicas_updates_table.py +76 -0
- rucio/db/sqla/migrate_repo/versions/3c9df354071b_extend_waiting_request_state.py +60 -0
- rucio/db/sqla/migrate_repo/versions/3d9813fab443_add_a_new_state_lost_in_badfilesstatus.py +44 -0
- rucio/db/sqla/migrate_repo/versions/40ad39ce3160_add_transferred_at_to_requests_table.py +43 -0
- rucio/db/sqla/migrate_repo/versions/4207be2fd914_add_notification_column_to_rules.py +64 -0
- rucio/db/sqla/migrate_repo/versions/42db2617c364_create_index_on_requests_external_id.py +40 -0
- rucio/db/sqla/migrate_repo/versions/436827b13f82_added_column_activity_to_table_requests.py +43 -0
- rucio/db/sqla/migrate_repo/versions/44278720f774_update_requests_typ_sta_upd_idx_index.py +44 -0
- rucio/db/sqla/migrate_repo/versions/45378a1e76a8_create_collection_replica_table.py +78 -0
- rucio/db/sqla/migrate_repo/versions/469d262be19_removing_created_at_index.py +41 -0
- rucio/db/sqla/migrate_repo/versions/4783c1f49cb4_create_distance_table.py +59 -0
- rucio/db/sqla/migrate_repo/versions/49a21b4d4357_create_index_on_table_tokens.py +44 -0
- rucio/db/sqla/migrate_repo/versions/4a2cbedda8b9_add_source_replica_expression_column_to_.py +43 -0
- rucio/db/sqla/migrate_repo/versions/4a7182d9578b_added_bytes_length_accessed_at_columns.py +49 -0
- rucio/db/sqla/migrate_repo/versions/4bab9edd01fc_create_index_on_requests_rule_id.py +40 -0
- rucio/db/sqla/migrate_repo/versions/4c3a4acfe006_new_attr_account_table.py +63 -0
- rucio/db/sqla/migrate_repo/versions/4cf0a2e127d4_adding_transient_metadata.py +43 -0
- rucio/db/sqla/migrate_repo/versions/4df2c5ddabc0_remove_temporary_dids.py +55 -0
- rucio/db/sqla/migrate_repo/versions/50280c53117c_add_qos_class_to_rse.py +45 -0
- rucio/db/sqla/migrate_repo/versions/52153819589c_add_rse_id_to_replicas_table.py +43 -0
- rucio/db/sqla/migrate_repo/versions/52fd9f4916fa_added_activity_to_rules.py +43 -0
- rucio/db/sqla/migrate_repo/versions/53b479c3cb0f_fix_did_meta_table_missing_updated_at_.py +45 -0
- rucio/db/sqla/migrate_repo/versions/5673b4b6e843_add_wfms_metadata_to_rule_tables.py +47 -0
- rucio/db/sqla/migrate_repo/versions/575767d9f89_added_source_history_table.py +58 -0
- rucio/db/sqla/migrate_repo/versions/58bff7008037_add_started_at_to_requests.py +45 -0
- rucio/db/sqla/migrate_repo/versions/58c8b78301ab_rename_callback_to_message.py +106 -0
- rucio/db/sqla/migrate_repo/versions/5f139f77382a_added_child_rule_id_column.py +55 -0
- rucio/db/sqla/migrate_repo/versions/688ef1840840_adding_did_meta_table.py +50 -0
- rucio/db/sqla/migrate_repo/versions/6e572a9bfbf3_add_new_split_container_column_to_rules.py +47 -0
- rucio/db/sqla/migrate_repo/versions/70587619328_add_comment_column_for_subscriptions.py +43 -0
- rucio/db/sqla/migrate_repo/versions/739064d31565_remove_history_table_pks.py +41 -0
- rucio/db/sqla/migrate_repo/versions/7541902bf173_add_didsfollowed_and_followevents_table.py +91 -0
- rucio/db/sqla/migrate_repo/versions/7ec22226cdbf_new_replica_state_for_temporary_.py +72 -0
- rucio/db/sqla/migrate_repo/versions/810a41685bc1_added_columns_rse_transfer_limits.py +49 -0
- rucio/db/sqla/migrate_repo/versions/83f991c63a93_correct_rse_expression_length.py +43 -0
- rucio/db/sqla/migrate_repo/versions/8523998e2e76_increase_size_of_extended_attributes_.py +43 -0
- rucio/db/sqla/migrate_repo/versions/8ea9122275b1_adding_missing_function_based_indices.py +53 -0
- rucio/db/sqla/migrate_repo/versions/90f47792bb76_add_clob_payload_to_messages.py +45 -0
- rucio/db/sqla/migrate_repo/versions/914b8f02df38_new_table_for_lifetime_model_exceptions.py +68 -0
- rucio/db/sqla/migrate_repo/versions/94a5961ddbf2_add_estimator_columns.py +45 -0
- rucio/db/sqla/migrate_repo/versions/9a1b149a2044_add_saml_identity_type.py +94 -0
- rucio/db/sqla/migrate_repo/versions/9a45bc4ea66d_add_vp_table.py +54 -0
- rucio/db/sqla/migrate_repo/versions/9eb936a81eb1_true_is_true.py +72 -0
- rucio/db/sqla/migrate_repo/versions/a08fa8de1545_transfer_stats_table.py +55 -0
- rucio/db/sqla/migrate_repo/versions/a118956323f8_added_vo_table_and_vo_col_to_rse.py +76 -0
- rucio/db/sqla/migrate_repo/versions/a193a275255c_add_status_column_in_messages.py +47 -0
- rucio/db/sqla/migrate_repo/versions/a5f6f6e928a7_1_7_0.py +121 -0
- rucio/db/sqla/migrate_repo/versions/a616581ee47_added_columns_to_table_requests.py +59 -0
- rucio/db/sqla/migrate_repo/versions/a6eb23955c28_state_idx_non_functional.py +52 -0
- rucio/db/sqla/migrate_repo/versions/a74275a1ad30_added_global_quota_table.py +54 -0
- rucio/db/sqla/migrate_repo/versions/a93e4e47bda_heartbeats.py +64 -0
- rucio/db/sqla/migrate_repo/versions/ae2a56fcc89_added_comment_column_to_rules.py +49 -0
- rucio/db/sqla/migrate_repo/versions/b0070f3695c8_add_deletedidmeta_table.py +57 -0
- rucio/db/sqla/migrate_repo/versions/b4293a99f344_added_column_identity_to_table_tokens.py +43 -0
- rucio/db/sqla/migrate_repo/versions/b5493606bbf5_fix_primary_key_for_subscription_history.py +41 -0
- rucio/db/sqla/migrate_repo/versions/b7d287de34fd_removal_of_replicastate_source.py +91 -0
- rucio/db/sqla/migrate_repo/versions/b818052fa670_add_index_to_quarantined_replicas.py +40 -0
- rucio/db/sqla/migrate_repo/versions/b8caac94d7f0_add_comments_column_for_subscriptions_.py +43 -0
- rucio/db/sqla/migrate_repo/versions/b96a1c7e1cc4_new_bad_pfns_table_and_bad_replicas_.py +143 -0
- rucio/db/sqla/migrate_repo/versions/bb695f45c04_extend_request_state.py +76 -0
- rucio/db/sqla/migrate_repo/versions/bc68e9946deb_add_staging_timestamps_to_request.py +50 -0
- rucio/db/sqla/migrate_repo/versions/bf3baa1c1474_correct_pk_and_idx_for_history_tables.py +72 -0
- rucio/db/sqla/migrate_repo/versions/c0937668555f_add_qos_policy_map_table.py +55 -0
- rucio/db/sqla/migrate_repo/versions/c129ccdb2d5_add_lumiblocknr_to_dids.py +43 -0
- rucio/db/sqla/migrate_repo/versions/ccdbcd48206e_add_did_type_column_index_on_did_meta_.py +65 -0
- rucio/db/sqla/migrate_repo/versions/cebad904c4dd_new_payload_column_for_heartbeats.py +47 -0
- rucio/db/sqla/migrate_repo/versions/d1189a09c6e0_oauth2_0_and_jwt_feature_support_adding_.py +146 -0
- rucio/db/sqla/migrate_repo/versions/d23453595260_extend_request_state_for_preparer.py +104 -0
- rucio/db/sqla/migrate_repo/versions/d6dceb1de2d_added_purge_column_to_rules.py +44 -0
- rucio/db/sqla/migrate_repo/versions/d6e2c3b2cf26_remove_third_party_copy_column_from_rse.py +43 -0
- rucio/db/sqla/migrate_repo/versions/d91002c5841_new_account_limits_table.py +103 -0
- rucio/db/sqla/migrate_repo/versions/e138c364ebd0_extending_columns_for_filter_and_.py +49 -0
- rucio/db/sqla/migrate_repo/versions/e59300c8b179_support_for_archive.py +104 -0
- rucio/db/sqla/migrate_repo/versions/f1b14a8c2ac1_postgres_use_check_constraints.py +29 -0
- rucio/db/sqla/migrate_repo/versions/f41ffe206f37_oracle_global_temporary_tables.py +74 -0
- rucio/db/sqla/migrate_repo/versions/f85a2962b021_adding_transfertool_column_to_requests_.py +47 -0
- rucio/db/sqla/migrate_repo/versions/fa7a7d78b602_increase_refresh_token_size.py +43 -0
- rucio/db/sqla/migrate_repo/versions/fb28a95fe288_add_replicas_rse_id_tombstone_idx.py +37 -0
- rucio/db/sqla/migrate_repo/versions/fe1a65b176c9_set_third_party_copy_read_and_write_.py +43 -0
- rucio/db/sqla/migrate_repo/versions/fe8ea2fa9788_added_third_party_copy_column_to_rse_.py +43 -0
- rucio/db/sqla/models.py +1743 -0
- rucio/db/sqla/sautils.py +55 -0
- rucio/db/sqla/session.py +529 -0
- rucio/db/sqla/types.py +206 -0
- rucio/db/sqla/util.py +543 -0
- rucio/gateway/__init__.py +13 -0
- rucio/gateway/account.py +345 -0
- rucio/gateway/account_limit.py +363 -0
- rucio/gateway/authentication.py +381 -0
- rucio/gateway/config.py +227 -0
- rucio/gateway/credential.py +70 -0
- rucio/gateway/did.py +987 -0
- rucio/gateway/dirac.py +83 -0
- rucio/gateway/exporter.py +60 -0
- rucio/gateway/heartbeat.py +76 -0
- rucio/gateway/identity.py +189 -0
- rucio/gateway/importer.py +46 -0
- rucio/gateway/lifetime_exception.py +121 -0
- rucio/gateway/lock.py +153 -0
- rucio/gateway/meta_conventions.py +98 -0
- rucio/gateway/permission.py +74 -0
- rucio/gateway/quarantined_replica.py +79 -0
- rucio/gateway/replica.py +538 -0
- rucio/gateway/request.py +330 -0
- rucio/gateway/rse.py +632 -0
- rucio/gateway/rule.py +437 -0
- rucio/gateway/scope.py +100 -0
- rucio/gateway/subscription.py +280 -0
- rucio/gateway/vo.py +126 -0
- rucio/rse/__init__.py +96 -0
- rucio/rse/protocols/__init__.py +13 -0
- rucio/rse/protocols/bittorrent.py +194 -0
- rucio/rse/protocols/cache.py +111 -0
- rucio/rse/protocols/dummy.py +100 -0
- rucio/rse/protocols/gfal.py +708 -0
- rucio/rse/protocols/globus.py +243 -0
- rucio/rse/protocols/http_cache.py +82 -0
- rucio/rse/protocols/mock.py +123 -0
- rucio/rse/protocols/ngarc.py +209 -0
- rucio/rse/protocols/posix.py +250 -0
- rucio/rse/protocols/protocol.py +361 -0
- rucio/rse/protocols/rclone.py +365 -0
- rucio/rse/protocols/rfio.py +145 -0
- rucio/rse/protocols/srm.py +338 -0
- rucio/rse/protocols/ssh.py +414 -0
- rucio/rse/protocols/storm.py +195 -0
- rucio/rse/protocols/webdav.py +594 -0
- rucio/rse/protocols/xrootd.py +302 -0
- rucio/rse/rsemanager.py +881 -0
- rucio/rse/translation.py +260 -0
- rucio/tests/__init__.py +13 -0
- rucio/tests/common.py +280 -0
- rucio/tests/common_server.py +149 -0
- rucio/transfertool/__init__.py +13 -0
- rucio/transfertool/bittorrent.py +200 -0
- rucio/transfertool/bittorrent_driver.py +50 -0
- rucio/transfertool/bittorrent_driver_qbittorrent.py +134 -0
- rucio/transfertool/fts3.py +1600 -0
- rucio/transfertool/fts3_plugins.py +152 -0
- rucio/transfertool/globus.py +201 -0
- rucio/transfertool/globus_library.py +181 -0
- rucio/transfertool/mock.py +89 -0
- rucio/transfertool/transfertool.py +221 -0
- rucio/vcsversion.py +11 -0
- rucio/version.py +45 -0
- rucio/web/__init__.py +13 -0
- rucio/web/rest/__init__.py +13 -0
- rucio/web/rest/flaskapi/__init__.py +13 -0
- rucio/web/rest/flaskapi/authenticated_bp.py +27 -0
- rucio/web/rest/flaskapi/v1/__init__.py +13 -0
- rucio/web/rest/flaskapi/v1/accountlimits.py +236 -0
- rucio/web/rest/flaskapi/v1/accounts.py +1103 -0
- rucio/web/rest/flaskapi/v1/archives.py +102 -0
- rucio/web/rest/flaskapi/v1/auth.py +1644 -0
- rucio/web/rest/flaskapi/v1/common.py +426 -0
- rucio/web/rest/flaskapi/v1/config.py +304 -0
- rucio/web/rest/flaskapi/v1/credentials.py +213 -0
- rucio/web/rest/flaskapi/v1/dids.py +2340 -0
- rucio/web/rest/flaskapi/v1/dirac.py +116 -0
- rucio/web/rest/flaskapi/v1/export.py +75 -0
- rucio/web/rest/flaskapi/v1/heartbeats.py +127 -0
- rucio/web/rest/flaskapi/v1/identities.py +285 -0
- rucio/web/rest/flaskapi/v1/import.py +132 -0
- rucio/web/rest/flaskapi/v1/lifetime_exceptions.py +312 -0
- rucio/web/rest/flaskapi/v1/locks.py +358 -0
- rucio/web/rest/flaskapi/v1/main.py +91 -0
- rucio/web/rest/flaskapi/v1/meta_conventions.py +241 -0
- rucio/web/rest/flaskapi/v1/metrics.py +36 -0
- rucio/web/rest/flaskapi/v1/nongrid_traces.py +97 -0
- rucio/web/rest/flaskapi/v1/ping.py +88 -0
- rucio/web/rest/flaskapi/v1/redirect.py +366 -0
- rucio/web/rest/flaskapi/v1/replicas.py +1894 -0
- rucio/web/rest/flaskapi/v1/requests.py +998 -0
- rucio/web/rest/flaskapi/v1/rses.py +2250 -0
- rucio/web/rest/flaskapi/v1/rules.py +854 -0
- rucio/web/rest/flaskapi/v1/scopes.py +159 -0
- rucio/web/rest/flaskapi/v1/subscriptions.py +650 -0
- rucio/web/rest/flaskapi/v1/templates/auth_crash.html +80 -0
- rucio/web/rest/flaskapi/v1/templates/auth_granted.html +82 -0
- rucio/web/rest/flaskapi/v1/traces.py +137 -0
- rucio/web/rest/flaskapi/v1/types.py +20 -0
- rucio/web/rest/flaskapi/v1/vos.py +278 -0
- rucio/web/rest/main.py +18 -0
- rucio/web/rest/metrics.py +27 -0
- rucio/web/rest/ping.py +27 -0
- rucio-37.0.0rc1.data/data/rucio/etc/alembic.ini.template +71 -0
- rucio-37.0.0rc1.data/data/rucio/etc/alembic_offline.ini.template +74 -0
- rucio-37.0.0rc1.data/data/rucio/etc/globus-config.yml.template +5 -0
- rucio-37.0.0rc1.data/data/rucio/etc/ldap.cfg.template +30 -0
- rucio-37.0.0rc1.data/data/rucio/etc/mail_templates/rule_approval_request.tmpl +38 -0
- rucio-37.0.0rc1.data/data/rucio/etc/mail_templates/rule_approved_admin.tmpl +4 -0
- rucio-37.0.0rc1.data/data/rucio/etc/mail_templates/rule_approved_user.tmpl +17 -0
- rucio-37.0.0rc1.data/data/rucio/etc/mail_templates/rule_denied_admin.tmpl +6 -0
- rucio-37.0.0rc1.data/data/rucio/etc/mail_templates/rule_denied_user.tmpl +17 -0
- rucio-37.0.0rc1.data/data/rucio/etc/mail_templates/rule_ok_notification.tmpl +19 -0
- rucio-37.0.0rc1.data/data/rucio/etc/rse-accounts.cfg.template +25 -0
- rucio-37.0.0rc1.data/data/rucio/etc/rucio.cfg.atlas.client.template +43 -0
- rucio-37.0.0rc1.data/data/rucio/etc/rucio.cfg.template +241 -0
- rucio-37.0.0rc1.data/data/rucio/etc/rucio_multi_vo.cfg.template +217 -0
- rucio-37.0.0rc1.data/data/rucio/requirements.server.txt +297 -0
- rucio-37.0.0rc1.data/data/rucio/tools/bootstrap.py +34 -0
- rucio-37.0.0rc1.data/data/rucio/tools/merge_rucio_configs.py +144 -0
- rucio-37.0.0rc1.data/data/rucio/tools/reset_database.py +40 -0
- rucio-37.0.0rc1.data/scripts/rucio +133 -0
- rucio-37.0.0rc1.data/scripts/rucio-abacus-account +74 -0
- rucio-37.0.0rc1.data/scripts/rucio-abacus-collection-replica +46 -0
- rucio-37.0.0rc1.data/scripts/rucio-abacus-rse +78 -0
- rucio-37.0.0rc1.data/scripts/rucio-admin +97 -0
- rucio-37.0.0rc1.data/scripts/rucio-atropos +60 -0
- rucio-37.0.0rc1.data/scripts/rucio-auditor +206 -0
- rucio-37.0.0rc1.data/scripts/rucio-automatix +50 -0
- rucio-37.0.0rc1.data/scripts/rucio-bb8 +57 -0
- rucio-37.0.0rc1.data/scripts/rucio-cache-client +141 -0
- rucio-37.0.0rc1.data/scripts/rucio-cache-consumer +42 -0
- rucio-37.0.0rc1.data/scripts/rucio-conveyor-finisher +58 -0
- rucio-37.0.0rc1.data/scripts/rucio-conveyor-poller +66 -0
- rucio-37.0.0rc1.data/scripts/rucio-conveyor-preparer +37 -0
- rucio-37.0.0rc1.data/scripts/rucio-conveyor-receiver +44 -0
- rucio-37.0.0rc1.data/scripts/rucio-conveyor-stager +76 -0
- rucio-37.0.0rc1.data/scripts/rucio-conveyor-submitter +139 -0
- rucio-37.0.0rc1.data/scripts/rucio-conveyor-throttler +104 -0
- rucio-37.0.0rc1.data/scripts/rucio-dark-reaper +53 -0
- rucio-37.0.0rc1.data/scripts/rucio-dumper +160 -0
- rucio-37.0.0rc1.data/scripts/rucio-follower +44 -0
- rucio-37.0.0rc1.data/scripts/rucio-hermes +54 -0
- rucio-37.0.0rc1.data/scripts/rucio-judge-cleaner +89 -0
- rucio-37.0.0rc1.data/scripts/rucio-judge-evaluator +137 -0
- rucio-37.0.0rc1.data/scripts/rucio-judge-injector +44 -0
- rucio-37.0.0rc1.data/scripts/rucio-judge-repairer +44 -0
- rucio-37.0.0rc1.data/scripts/rucio-kronos +44 -0
- rucio-37.0.0rc1.data/scripts/rucio-minos +53 -0
- rucio-37.0.0rc1.data/scripts/rucio-minos-temporary-expiration +50 -0
- rucio-37.0.0rc1.data/scripts/rucio-necromancer +120 -0
- rucio-37.0.0rc1.data/scripts/rucio-oauth-manager +63 -0
- rucio-37.0.0rc1.data/scripts/rucio-reaper +83 -0
- rucio-37.0.0rc1.data/scripts/rucio-replica-recoverer +248 -0
- rucio-37.0.0rc1.data/scripts/rucio-rse-decommissioner +66 -0
- rucio-37.0.0rc1.data/scripts/rucio-storage-consistency-actions +74 -0
- rucio-37.0.0rc1.data/scripts/rucio-transmogrifier +77 -0
- rucio-37.0.0rc1.data/scripts/rucio-undertaker +76 -0
- rucio-37.0.0rc1.dist-info/METADATA +92 -0
- rucio-37.0.0rc1.dist-info/RECORD +487 -0
- rucio-37.0.0rc1.dist-info/WHEEL +5 -0
- rucio-37.0.0rc1.dist-info/licenses/AUTHORS.rst +100 -0
- rucio-37.0.0rc1.dist-info/licenses/LICENSE +201 -0
- rucio-37.0.0rc1.dist-info/top_level.txt +1 -0
rucio/core/rule.py
ADDED
|
@@ -0,0 +1,4484 @@
|
|
|
1
|
+
# Copyright European Organization for Nuclear Research (CERN) since 2012
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
from configparser import NoOptionError, NoSectionError
|
|
18
|
+
from copy import deepcopy
|
|
19
|
+
from datetime import datetime, timedelta
|
|
20
|
+
from os import path
|
|
21
|
+
from re import match
|
|
22
|
+
from string import Template
|
|
23
|
+
from typing import TYPE_CHECKING, Any, Literal, Optional, TypeVar, Union
|
|
24
|
+
|
|
25
|
+
from dogpile.cache.api import NoValue
|
|
26
|
+
from sqlalchemy import delete, desc, select, update
|
|
27
|
+
from sqlalchemy.exc import (
|
|
28
|
+
IntegrityError,
|
|
29
|
+
NoResultFound, # https://pydoc.dev/sqlalchemy/latest/sqlalchemy.exc.NoResultFound.html
|
|
30
|
+
StatementError,
|
|
31
|
+
)
|
|
32
|
+
from sqlalchemy.sql import func
|
|
33
|
+
from sqlalchemy.sql.expression import and_, false, null, or_, true, tuple_
|
|
34
|
+
|
|
35
|
+
import rucio.core.did
|
|
36
|
+
import rucio.core.lock # import get_replica_locks, get_files_and_replica_locks_of_dataset
|
|
37
|
+
import rucio.core.replica # import get_and_lock_file_replicas, get_and_lock_file_replicas_for_dataset
|
|
38
|
+
from rucio.common.cache import MemcacheRegion
|
|
39
|
+
from rucio.common.config import config_get
|
|
40
|
+
from rucio.common.constants import RseAttr
|
|
41
|
+
from rucio.common.exception import (
|
|
42
|
+
DataIdentifierNotFound,
|
|
43
|
+
DuplicateRule,
|
|
44
|
+
InputValidationError,
|
|
45
|
+
InsufficientAccountLimit,
|
|
46
|
+
InsufficientTargetRSEs,
|
|
47
|
+
InvalidObject,
|
|
48
|
+
InvalidReplicationRule,
|
|
49
|
+
InvalidRSEExpression,
|
|
50
|
+
InvalidRuleWeight,
|
|
51
|
+
InvalidSourceReplicaExpression,
|
|
52
|
+
InvalidValueForKey,
|
|
53
|
+
ManualRuleApprovalBlocked,
|
|
54
|
+
ReplicationRuleCreationTemporaryFailed,
|
|
55
|
+
RequestNotFound,
|
|
56
|
+
RSEOverQuota,
|
|
57
|
+
RSEWriteBlocked,
|
|
58
|
+
RucioException,
|
|
59
|
+
RuleNotFound,
|
|
60
|
+
RuleReplaceFailed,
|
|
61
|
+
StagingAreaRuleRequiresLifetime,
|
|
62
|
+
UndefinedPolicy,
|
|
63
|
+
UnsupportedOperation,
|
|
64
|
+
)
|
|
65
|
+
from rucio.common.plugins import PolicyPackageAlgorithms
|
|
66
|
+
from rucio.common.policy import get_scratchdisk_lifetime, policy_filter
|
|
67
|
+
from rucio.common.schema import validate_schema
|
|
68
|
+
from rucio.common.types import DIDDict, InternalAccount, InternalScope, LoggerFunction, RuleDict
|
|
69
|
+
from rucio.common.utils import chunks, sizefmt, str_to_date
|
|
70
|
+
from rucio.core import account_counter, rse_counter
|
|
71
|
+
from rucio.core import request as request_core
|
|
72
|
+
from rucio.core import transfer as transfer_core
|
|
73
|
+
from rucio.core.account import get_account, has_account_attribute
|
|
74
|
+
from rucio.core.lifetime_exception import define_eol
|
|
75
|
+
from rucio.core.message import add_message
|
|
76
|
+
from rucio.core.monitor import MetricManager
|
|
77
|
+
from rucio.core.rse import get_rse, get_rse_name, get_rse_usage, list_rse_attributes
|
|
78
|
+
from rucio.core.rse_expression_parser import parse_expression
|
|
79
|
+
from rucio.core.rse_selector import RSESelector
|
|
80
|
+
from rucio.core.rule_grouping import apply_rule, apply_rule_grouping, create_transfer_dict, repair_stuck_locks_and_apply_rule_grouping
|
|
81
|
+
from rucio.db.sqla import filter_thread_work, models
|
|
82
|
+
from rucio.db.sqla.constants import OBSOLETE, BadFilesStatus, DIDAvailability, DIDReEvaluation, DIDType, LockState, ReplicaState, RequestType, RSEType, RuleGrouping, RuleNotification, RuleState
|
|
83
|
+
from rucio.db.sqla.session import read_session, stream_session, transactional_session
|
|
84
|
+
|
|
85
|
+
if TYPE_CHECKING:
|
|
86
|
+
from collections.abc import Callable, Iterator, Sequence
|
|
87
|
+
|
|
88
|
+
from sqlalchemy.orm import Session
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
REGION = MemcacheRegion(expiration_time=900)
|
|
92
|
+
METRICS = MetricManager(module=__name__)
|
|
93
|
+
AutoApproveT = TypeVar('AutoApproveT', bound='AutoApprove')
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class AutoApprove(PolicyPackageAlgorithms):
|
|
97
|
+
"""
|
|
98
|
+
Handle automatic approval algorithms for replication rules
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
_algorithm_type = 'auto_approve'
|
|
102
|
+
|
|
103
|
+
def __init__(self, rule: models.ReplicationRule, did: models.DataIdentifier, session: 'Session') -> None:
|
|
104
|
+
super().__init__()
|
|
105
|
+
self.rule = rule
|
|
106
|
+
self.did = did
|
|
107
|
+
self.session = session
|
|
108
|
+
self.register("default", self.default)
|
|
109
|
+
|
|
110
|
+
def evaluate(self) -> bool:
|
|
111
|
+
"""
|
|
112
|
+
Evaluate the auto-approve algorithm
|
|
113
|
+
"""
|
|
114
|
+
return self.get_configured_algorithm()(self.rule, self.did, self.session)
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def get_configured_algorithm(cls: type[AutoApproveT]) -> "Callable[[models.ReplicationRule, models.DataIdentifier, Session], bool]":
|
|
118
|
+
"""
|
|
119
|
+
Get the configured auto-approve algorithm
|
|
120
|
+
"""
|
|
121
|
+
try:
|
|
122
|
+
configured_algorithm: str = str(config_get('rules', cls._algorithm_type, default='default'))
|
|
123
|
+
except (NoOptionError, NoSectionError, RuntimeError):
|
|
124
|
+
configured_algorithm = 'default'
|
|
125
|
+
|
|
126
|
+
return super()._get_one_algorithm(cls._algorithm_type, configured_algorithm)
|
|
127
|
+
|
|
128
|
+
@classmethod
|
|
129
|
+
def register(cls: type[AutoApproveT], name: str, fn_auto_approve: "Callable[[models.ReplicationRule, models.DataIdentifier, Session], bool]") -> None:
|
|
130
|
+
"""
|
|
131
|
+
Register a new auto-approve algorithm
|
|
132
|
+
"""
|
|
133
|
+
algorithm_dict = {name: fn_auto_approve}
|
|
134
|
+
super()._register(cls._algorithm_type, algorithm_dict)
|
|
135
|
+
|
|
136
|
+
@staticmethod
|
|
137
|
+
def default(rule: models.ReplicationRule, did: models.DataIdentifier, session: 'Session') -> bool:
|
|
138
|
+
"""
|
|
139
|
+
Default auto-approve algorithm
|
|
140
|
+
"""
|
|
141
|
+
rse_expression = rule['rse_expression']
|
|
142
|
+
vo = rule['account'].vo
|
|
143
|
+
|
|
144
|
+
rses = parse_expression(rse_expression, filter_={'vo': vo}, session=session)
|
|
145
|
+
|
|
146
|
+
auto_approve = False
|
|
147
|
+
# Block manual approval for multi-rse rules
|
|
148
|
+
if len(rses) > 1:
|
|
149
|
+
raise InvalidReplicationRule('Ask approval is not allowed for rules with multiple RSEs')
|
|
150
|
+
if len(rses) == 1 and not did.is_open and did.bytes is not None and did.length is not None:
|
|
151
|
+
# This rule can be considered for auto-approval:
|
|
152
|
+
rse_attr = list_rse_attributes(rse_id=rses[0]['id'], session=session)
|
|
153
|
+
auto_approve = False
|
|
154
|
+
if RseAttr.AUTO_APPROVE_BYTES in rse_attr and RseAttr.AUTO_APPROVE_FILES in rse_attr:
|
|
155
|
+
if did.bytes < int(rse_attr.get(RseAttr.AUTO_APPROVE_BYTES)) and did.length < int(rse_attr.get(RseAttr.AUTO_APPROVE_FILES)):
|
|
156
|
+
auto_approve = True
|
|
157
|
+
elif did.bytes < int(rse_attr.get(RseAttr.AUTO_APPROVE_BYTES, -1)):
|
|
158
|
+
auto_approve = True
|
|
159
|
+
elif did.length < int(rse_attr.get(RseAttr.AUTO_APPROVE_FILES, -1)):
|
|
160
|
+
auto_approve = True
|
|
161
|
+
|
|
162
|
+
return auto_approve
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@transactional_session
|
|
166
|
+
def add_rule(
|
|
167
|
+
dids: 'Sequence[DIDDict]',
|
|
168
|
+
account: InternalAccount,
|
|
169
|
+
copies: int,
|
|
170
|
+
rse_expression: str,
|
|
171
|
+
grouping: Literal['ALL', 'DATASET', 'NONE'],
|
|
172
|
+
weight: Optional[str],
|
|
173
|
+
lifetime: Optional[int],
|
|
174
|
+
locked: bool,
|
|
175
|
+
subscription_id: Optional[str],
|
|
176
|
+
source_replica_expression: Optional[str] = None,
|
|
177
|
+
activity: str = 'User Subscriptions',
|
|
178
|
+
notify: Optional[Literal['Y', 'N', 'C', 'P']] = None,
|
|
179
|
+
purge_replicas: bool = False,
|
|
180
|
+
ignore_availability: bool = False,
|
|
181
|
+
comment: Optional[str] = None,
|
|
182
|
+
ask_approval: bool = False,
|
|
183
|
+
asynchronous: bool = False,
|
|
184
|
+
ignore_account_limit: bool = False,
|
|
185
|
+
priority: int = 3,
|
|
186
|
+
delay_injection: Optional[int] = None,
|
|
187
|
+
split_container: bool = False,
|
|
188
|
+
meta: Optional[dict[str, Any]] = None,
|
|
189
|
+
*,
|
|
190
|
+
session: "Session",
|
|
191
|
+
logger: LoggerFunction = logging.log
|
|
192
|
+
) -> list[str]:
|
|
193
|
+
"""
|
|
194
|
+
Adds a replication rule for every did in dids
|
|
195
|
+
|
|
196
|
+
:param dids: List of data identifiers.
|
|
197
|
+
:param account: Account issuing the rule.
|
|
198
|
+
:param copies: The number of replicas.
|
|
199
|
+
:param rse_expression: RSE expression which gets resolved into a list of rses.
|
|
200
|
+
:param grouping: ALL - All files will be replicated to the same RSE.
|
|
201
|
+
DATASET - All files in the same dataset will be replicated to the same RSE.
|
|
202
|
+
NONE - Files will be completely spread over all allowed RSEs without any grouping considerations at all.
|
|
203
|
+
:param weight: Weighting scheme to be used.
|
|
204
|
+
:param lifetime: The lifetime of the replication rule in seconds.
|
|
205
|
+
:param locked: If the rule is locked.
|
|
206
|
+
:param subscription_id: The subscription_id, if the rule is created by a subscription.
|
|
207
|
+
:param source_replica_expression: Only use replicas as source from this RSEs.
|
|
208
|
+
:param activity: Activity to be passed on to the conveyor.
|
|
209
|
+
:param notify: Notification setting of the rule ('Y', 'N', 'C'; None = 'N').
|
|
210
|
+
:param purge_replicas: Purge setting if a replica should be directly deleted after the rule is deleted.
|
|
211
|
+
:param ignore_availability: Option to ignore the availability of RSEs.
|
|
212
|
+
:param comment: Comment about the rule.
|
|
213
|
+
:param ask_approval: Ask for approval for this rule.
|
|
214
|
+
:param asynchronous: Create replication rule asynchronously by the judge-injector.
|
|
215
|
+
:param delay_injection: Create replication after 'delay' seconds. Implies asynchronous=True.
|
|
216
|
+
:param ignore_account_limit: Ignore quota and create the rule outside of the account limits.
|
|
217
|
+
:param priority: Priority of the rule and the transfers which should be submitted.
|
|
218
|
+
:param split_container: Should a container rule be split into individual dataset rules.
|
|
219
|
+
:param meta: Dictionary with metadata from the WFMS.
|
|
220
|
+
:param session: The database session in use.
|
|
221
|
+
:param logger: Optional decorated logger that can be passed from the calling daemons or servers.
|
|
222
|
+
:returns: A list of created replication rule ids.
|
|
223
|
+
:raises: InvalidReplicationRule, InsufficientAccountLimit, InvalidRSEExpression, DataIdentifierNotFound, ReplicationRuleCreationTemporaryFailed, InvalidRuleWeight,
|
|
224
|
+
StagingAreaRuleRequiresLifetime, DuplicateRule, RSEWriteBlocked, ScratchDiskLifetimeConflict, ManualRuleApprovalBlocked, RSEOverQuota
|
|
225
|
+
"""
|
|
226
|
+
if copies <= 0:
|
|
227
|
+
raise InvalidValueForKey("The number of copies for a replication rule should be greater than 0.")
|
|
228
|
+
|
|
229
|
+
rule_ids = []
|
|
230
|
+
|
|
231
|
+
grouping_value = {'ALL': RuleGrouping.ALL, 'NONE': RuleGrouping.NONE}.get(grouping, RuleGrouping.DATASET)
|
|
232
|
+
|
|
233
|
+
with METRICS.timer('add_rule.total'):
|
|
234
|
+
# 1. Resolve the rse_expression into a list of RSE-ids
|
|
235
|
+
with METRICS.timer('add_rule.parse_rse_expression'):
|
|
236
|
+
vo = account.vo
|
|
237
|
+
if ignore_availability:
|
|
238
|
+
rses = parse_expression(rse_expression, filter_={'vo': vo}, session=session)
|
|
239
|
+
else:
|
|
240
|
+
rses = parse_expression(rse_expression, filter_={'vo': vo, 'availability_write': True}, session=session)
|
|
241
|
+
|
|
242
|
+
if lifetime is None: # Check if one of the rses is a staging area
|
|
243
|
+
if [rse for rse in rses if rse.get('staging_area', False)]:
|
|
244
|
+
raise StagingAreaRuleRequiresLifetime('Rules for a staging area must include a lifetime')
|
|
245
|
+
|
|
246
|
+
# Check SCRATCHDISK Policy
|
|
247
|
+
try:
|
|
248
|
+
lifetime = get_scratch_policy(account, rses, lifetime, session=session)
|
|
249
|
+
except UndefinedPolicy:
|
|
250
|
+
pass
|
|
251
|
+
|
|
252
|
+
# Auto-lock rules for TAPE rses
|
|
253
|
+
if not locked and lifetime is None:
|
|
254
|
+
if [rse for rse in rses if rse.get('rse_type', RSEType.DISK) == RSEType.TAPE]:
|
|
255
|
+
locked = True
|
|
256
|
+
|
|
257
|
+
# Block manual approval if RSE does not allow it
|
|
258
|
+
if ask_approval:
|
|
259
|
+
for rse in rses:
|
|
260
|
+
if list_rse_attributes(rse_id=rse['id'], session=session).get(RseAttr.BLOCK_MANUAL_APPROVAL, False):
|
|
261
|
+
raise ManualRuleApprovalBlocked()
|
|
262
|
+
|
|
263
|
+
if source_replica_expression:
|
|
264
|
+
try:
|
|
265
|
+
source_rses = parse_expression(source_replica_expression, filter_={'vo': vo}, session=session)
|
|
266
|
+
except InvalidRSEExpression as exc:
|
|
267
|
+
raise InvalidSourceReplicaExpression from exc
|
|
268
|
+
else:
|
|
269
|
+
source_rses = []
|
|
270
|
+
|
|
271
|
+
# 2. Create the rse selector
|
|
272
|
+
with METRICS.timer('add_rule.create_rse_selector'):
|
|
273
|
+
rseselector = RSESelector(account=account, rses=rses, weight=weight, copies=copies, ignore_account_limit=ask_approval or ignore_account_limit, session=session)
|
|
274
|
+
|
|
275
|
+
expires_at = datetime.utcnow() + timedelta(seconds=lifetime) if lifetime is not None else None
|
|
276
|
+
|
|
277
|
+
notify_value = {'Y': RuleNotification.YES, 'C': RuleNotification.CLOSE, 'P': RuleNotification.PROGRESS}.get(notify or '', RuleNotification.NO)
|
|
278
|
+
|
|
279
|
+
for elem in dids:
|
|
280
|
+
# 3. Get the did
|
|
281
|
+
with METRICS.timer('add_rule.get_did'):
|
|
282
|
+
try:
|
|
283
|
+
stmt = select(
|
|
284
|
+
models.DataIdentifier
|
|
285
|
+
).where(
|
|
286
|
+
and_(models.DataIdentifier.scope == elem['scope'],
|
|
287
|
+
models.DataIdentifier.name == elem['name'])
|
|
288
|
+
)
|
|
289
|
+
did = session.execute(stmt).scalar_one()
|
|
290
|
+
except NoResultFound as exc:
|
|
291
|
+
raise DataIdentifierNotFound('Data identifier %s:%s is not valid.' % (elem['scope'], elem['name'])) from exc
|
|
292
|
+
except TypeError as error:
|
|
293
|
+
raise InvalidObject(error.args) from error # https://pylint.readthedocs.io/en/latest/user_guide/messages/warning/raise-missing-from.html
|
|
294
|
+
|
|
295
|
+
# 3.1 If the did is a constituent, relay the rule to the archive
|
|
296
|
+
if did.did_type == DIDType.FILE and did.constituent:
|
|
297
|
+
# Check if a single replica of this DID exists; Do not put rule on file if there are only replicas on TAPE
|
|
298
|
+
stmt = select(
|
|
299
|
+
func.count()
|
|
300
|
+
).select_from(
|
|
301
|
+
models.RSEFileAssociation
|
|
302
|
+
).join(
|
|
303
|
+
models.RSE,
|
|
304
|
+
models.RSEFileAssociation.rse_id == models.RSE.id
|
|
305
|
+
).where(
|
|
306
|
+
and_(models.RSEFileAssociation.scope == did.scope,
|
|
307
|
+
models.RSEFileAssociation.name == did.name,
|
|
308
|
+
models.RSEFileAssociation.state == ReplicaState.AVAILABLE,
|
|
309
|
+
models.RSE.rse_type != RSEType.TAPE)
|
|
310
|
+
)
|
|
311
|
+
replica_cnt = session.execute(stmt).scalar()
|
|
312
|
+
|
|
313
|
+
if replica_cnt == 0: # Put the rule on the archive
|
|
314
|
+
stmt = select(
|
|
315
|
+
models.ConstituentAssociation
|
|
316
|
+
).join(
|
|
317
|
+
models.RSEFileAssociation,
|
|
318
|
+
and_(models.ConstituentAssociation.scope == models.RSEFileAssociation.scope,
|
|
319
|
+
models.ConstituentAssociation.name == models.RSEFileAssociation.name)
|
|
320
|
+
).where(
|
|
321
|
+
and_(models.ConstituentAssociation.child_scope == did.scope,
|
|
322
|
+
models.ConstituentAssociation.child_name == did.name)
|
|
323
|
+
)
|
|
324
|
+
archive = session.execute(stmt).scalars().first()
|
|
325
|
+
if archive is not None:
|
|
326
|
+
elem['name'] = archive.name
|
|
327
|
+
elem['scope'] = archive.scope
|
|
328
|
+
try:
|
|
329
|
+
stmt = select(
|
|
330
|
+
models.DataIdentifier
|
|
331
|
+
).where(
|
|
332
|
+
and_(models.DataIdentifier.scope == elem['scope'],
|
|
333
|
+
models.DataIdentifier.name == elem['name'])
|
|
334
|
+
)
|
|
335
|
+
did = session.execute(stmt).scalar_one()
|
|
336
|
+
except NoResultFound as exc:
|
|
337
|
+
raise DataIdentifierNotFound('Data identifier %s:%s is not valid.' % (elem['scope'], elem['name'])) from exc
|
|
338
|
+
except TypeError as error:
|
|
339
|
+
raise InvalidObject(error.args) from error
|
|
340
|
+
else: # Put the rule on the constituent directly
|
|
341
|
+
pass
|
|
342
|
+
|
|
343
|
+
# 3.2 Get the lifetime
|
|
344
|
+
eol_at = define_eol(elem['scope'], elem['name'], rses, session=session)
|
|
345
|
+
|
|
346
|
+
# 4. Create the replication rule
|
|
347
|
+
with METRICS.timer('add_rule.create_rule'):
|
|
348
|
+
meta_json = None
|
|
349
|
+
if meta is not None:
|
|
350
|
+
try:
|
|
351
|
+
meta_json = json.dumps(meta)
|
|
352
|
+
except Exception:
|
|
353
|
+
meta_json = None
|
|
354
|
+
|
|
355
|
+
new_rule = models.ReplicationRule(account=account,
|
|
356
|
+
name=elem['name'],
|
|
357
|
+
scope=elem['scope'],
|
|
358
|
+
did_type=did.did_type,
|
|
359
|
+
copies=copies,
|
|
360
|
+
rse_expression=rse_expression,
|
|
361
|
+
locked=locked,
|
|
362
|
+
grouping=grouping_value,
|
|
363
|
+
expires_at=expires_at,
|
|
364
|
+
weight=weight,
|
|
365
|
+
source_replica_expression=source_replica_expression,
|
|
366
|
+
activity=activity,
|
|
367
|
+
subscription_id=subscription_id,
|
|
368
|
+
notification=notify_value,
|
|
369
|
+
purge_replicas=purge_replicas,
|
|
370
|
+
ignore_availability=ignore_availability,
|
|
371
|
+
comments=comment,
|
|
372
|
+
ignore_account_limit=ignore_account_limit,
|
|
373
|
+
priority=priority,
|
|
374
|
+
split_container=split_container,
|
|
375
|
+
meta=meta_json,
|
|
376
|
+
eol_at=eol_at)
|
|
377
|
+
try:
|
|
378
|
+
new_rule.save(session=session)
|
|
379
|
+
except IntegrityError as error:
|
|
380
|
+
if match('.*ORA-00001.*', str(error.args[0])) \
|
|
381
|
+
or match('.*IntegrityError.*UNIQUE constraint failed.*', str(error.args[0])) \
|
|
382
|
+
or match('.*1062.*Duplicate entry.*for key.*', str(error.args[0])) \
|
|
383
|
+
or match('.*IntegrityError.*duplicate key value violates unique constraint.*', error.args[0]) \
|
|
384
|
+
or match('.*UniqueViolation.*duplicate key value violates unique constraint.*', error.args[0]) \
|
|
385
|
+
or match('.*IntegrityError.*columns? .*not unique.*', error.args[0]):
|
|
386
|
+
raise DuplicateRule(error.args[0]) from error
|
|
387
|
+
raise InvalidReplicationRule(error.args[0]) from error
|
|
388
|
+
rule_ids.append(new_rule.id)
|
|
389
|
+
|
|
390
|
+
if ask_approval:
|
|
391
|
+
new_rule.state = RuleState.WAITING_APPROVAL
|
|
392
|
+
# Use the new rule as the argument here
|
|
393
|
+
auto_approver = AutoApprove(new_rule, did, session=session)
|
|
394
|
+
if auto_approver.evaluate():
|
|
395
|
+
logger(logging.DEBUG, "Auto approving rule %s", str(new_rule.id))
|
|
396
|
+
logger(logging.DEBUG, "Created rule %s for injection", str(new_rule.id))
|
|
397
|
+
approve_rule(rule_id=new_rule.id, notify_approvers=False, session=session)
|
|
398
|
+
continue
|
|
399
|
+
|
|
400
|
+
logger(logging.DEBUG, "Created rule %s in waiting for approval", str(new_rule.id))
|
|
401
|
+
__create_rule_approval_email(rule=new_rule, session=session)
|
|
402
|
+
continue
|
|
403
|
+
|
|
404
|
+
# Force ASYNC mode for large rules
|
|
405
|
+
if did.length is not None and (did.length * copies) >= 10000:
|
|
406
|
+
asynchronous = True
|
|
407
|
+
logger(logging.DEBUG, "Forced injection of rule %s", str(new_rule.id))
|
|
408
|
+
|
|
409
|
+
if asynchronous or delay_injection:
|
|
410
|
+
# TODO: asynchronous mode only available for closed dids (on the whole tree?)
|
|
411
|
+
new_rule.state = RuleState.INJECT
|
|
412
|
+
logger(logging.DEBUG, "Created rule %s for injection", str(new_rule.id))
|
|
413
|
+
if delay_injection:
|
|
414
|
+
new_rule.created_at = datetime.utcnow() + timedelta(seconds=delay_injection)
|
|
415
|
+
logger(logging.DEBUG, "Scheduled rule %s for injection on %s", str(new_rule.id), new_rule.created_at)
|
|
416
|
+
continue
|
|
417
|
+
|
|
418
|
+
# If Split Container is chosen, the rule will be processed ASYNC
|
|
419
|
+
if split_container and did.did_type == DIDType.CONTAINER:
|
|
420
|
+
new_rule.state = RuleState.INJECT
|
|
421
|
+
logger(logging.DEBUG, "Created rule %s for injection due to Split Container mode", str(new_rule.id))
|
|
422
|
+
continue
|
|
423
|
+
|
|
424
|
+
# 5. Apply the rule
|
|
425
|
+
with METRICS.timer('add_rule.apply_rule'):
|
|
426
|
+
try:
|
|
427
|
+
apply_rule(did, new_rule, [x['id'] for x in rses], [x['id'] for x in source_rses], rseselector, session=session)
|
|
428
|
+
except IntegrityError as error:
|
|
429
|
+
raise ReplicationRuleCreationTemporaryFailed(error.args[0]) from error
|
|
430
|
+
|
|
431
|
+
if new_rule.locks_stuck_cnt > 0:
|
|
432
|
+
new_rule.state = RuleState.STUCK
|
|
433
|
+
new_rule.error = 'MissingSourceReplica'
|
|
434
|
+
if new_rule.grouping != RuleGrouping.NONE:
|
|
435
|
+
stmt = update(
|
|
436
|
+
models.DatasetLock
|
|
437
|
+
).where(
|
|
438
|
+
models.DatasetLock.rule_id == new_rule.id
|
|
439
|
+
).values({
|
|
440
|
+
models.DatasetLock.state: LockState.STUCK
|
|
441
|
+
})
|
|
442
|
+
session.execute(stmt)
|
|
443
|
+
elif new_rule.locks_replicating_cnt == 0:
|
|
444
|
+
new_rule.state = RuleState.OK
|
|
445
|
+
if new_rule.grouping != RuleGrouping.NONE:
|
|
446
|
+
stmt = update(
|
|
447
|
+
models.DatasetLock
|
|
448
|
+
).where(
|
|
449
|
+
models.DatasetLock.rule_id == new_rule.id
|
|
450
|
+
).values({
|
|
451
|
+
models.DatasetLock.state: LockState.OK
|
|
452
|
+
})
|
|
453
|
+
session.execute(stmt)
|
|
454
|
+
session.flush()
|
|
455
|
+
if new_rule.notification == RuleNotification.YES:
|
|
456
|
+
generate_email_for_rule_ok_notification(rule=new_rule, session=session)
|
|
457
|
+
generate_rule_notifications(rule=new_rule, replicating_locks_before=0, session=session)
|
|
458
|
+
else:
|
|
459
|
+
new_rule.state = RuleState.REPLICATING
|
|
460
|
+
if new_rule.grouping != RuleGrouping.NONE:
|
|
461
|
+
stmt = update(
|
|
462
|
+
models.DatasetLock
|
|
463
|
+
).where(
|
|
464
|
+
models.DatasetLock.rule_id == new_rule.id
|
|
465
|
+
).values({
|
|
466
|
+
models.DatasetLock.state: LockState.REPLICATING
|
|
467
|
+
})
|
|
468
|
+
session.execute(stmt)
|
|
469
|
+
|
|
470
|
+
# Add rule to History
|
|
471
|
+
insert_rule_history(rule=new_rule, recent=True, longterm=True, session=session)
|
|
472
|
+
|
|
473
|
+
logger(logging.INFO, "Created rule %s [%d/%d/%d] with new algorithm for did %s:%s in state %s", str(new_rule.id), new_rule.locks_ok_cnt,
|
|
474
|
+
new_rule.locks_replicating_cnt, new_rule.locks_stuck_cnt, new_rule.scope, new_rule.name, str(new_rule.state))
|
|
475
|
+
|
|
476
|
+
return rule_ids
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
@transactional_session
|
|
480
|
+
def add_rules(
|
|
481
|
+
dids: 'Sequence[DIDDict]',
|
|
482
|
+
rules: 'Sequence[RuleDict]',
|
|
483
|
+
*,
|
|
484
|
+
session: "Session",
|
|
485
|
+
logger: LoggerFunction = logging.log
|
|
486
|
+
) -> dict[tuple[InternalScope, str], list[str]]:
|
|
487
|
+
"""
|
|
488
|
+
Adds a list of replication rules to every did in dids
|
|
489
|
+
|
|
490
|
+
:params dids: List of data identifiers.
|
|
491
|
+
:param rules: List of dictionaries defining replication rules.
|
|
492
|
+
{account, copies, rse_expression, grouping, weight, lifetime, locked, subscription_id, source_replica_expression, activity, notify, purge_replicas}
|
|
493
|
+
:param session: The database session in use.
|
|
494
|
+
:param logger: Optional decorated logger that can be passed from the calling daemons or servers.
|
|
495
|
+
:returns: Dictionary (scope, name) with list of created rule ids
|
|
496
|
+
:raises: InvalidReplicationRule, InsufficientAccountLimit, InvalidRSEExpression, DataIdentifierNotFound, ReplicationRuleCreationTemporaryFailed, InvalidRuleWeight,
|
|
497
|
+
StagingAreaRuleRequiresLifetime, DuplicateRule, RSEWriteBlocked, ScratchDiskLifetimeConflict, ManualRuleApprovalBlocked
|
|
498
|
+
"""
|
|
499
|
+
if any(r.get("copies", 1) <= 0 for r in rules):
|
|
500
|
+
raise InvalidValueForKey("The number of copies for a replication rule should be greater than 0.")
|
|
501
|
+
|
|
502
|
+
with METRICS.timer('add_rules.total'):
|
|
503
|
+
rule_ids = {}
|
|
504
|
+
|
|
505
|
+
# 1. Fetch the RSEs from the RSE expression to restrict further queries just on these RSEs
|
|
506
|
+
restrict_rses = []
|
|
507
|
+
all_source_rses = []
|
|
508
|
+
with METRICS.timer('add_rules.parse_rse_expressions'):
|
|
509
|
+
for rule in rules:
|
|
510
|
+
vo = rule['account'].vo
|
|
511
|
+
if rule.get('ignore_availability'):
|
|
512
|
+
restrict_rses.extend(parse_expression(rule['rse_expression'], filter_={'vo': vo}, session=session))
|
|
513
|
+
else:
|
|
514
|
+
restrict_rses.extend(parse_expression(rule['rse_expression'], filter_={'vo': vo, 'availability_write': True}, session=session))
|
|
515
|
+
restrict_rses = list(set([rse['id'] for rse in restrict_rses]))
|
|
516
|
+
|
|
517
|
+
for rule in rules:
|
|
518
|
+
if rule.get('source_replica_expression'):
|
|
519
|
+
vo = rule['account'].vo
|
|
520
|
+
all_source_rses.extend(parse_expression(rule.get('source_replica_expression'), filter_={'vo': vo}, session=session))
|
|
521
|
+
all_source_rses = list(set([rse['id'] for rse in all_source_rses]))
|
|
522
|
+
|
|
523
|
+
for elem in dids:
|
|
524
|
+
# 2. Get the did
|
|
525
|
+
with METRICS.timer('add_rules.get_did'):
|
|
526
|
+
try:
|
|
527
|
+
stmt = select(
|
|
528
|
+
models.DataIdentifier
|
|
529
|
+
).where(
|
|
530
|
+
and_(models.DataIdentifier.scope == elem['scope'],
|
|
531
|
+
models.DataIdentifier.name == elem['name'])
|
|
532
|
+
)
|
|
533
|
+
did = session.execute(stmt).scalar_one()
|
|
534
|
+
except NoResultFound as exc:
|
|
535
|
+
raise DataIdentifierNotFound('Data identifier %s:%s is not valid.' % (elem['scope'], elem['name'])) from exc
|
|
536
|
+
except TypeError as error:
|
|
537
|
+
raise InvalidObject(error.args) from error
|
|
538
|
+
|
|
539
|
+
# 2.1 If the did is a constituent, relay the rule to the archive
|
|
540
|
+
if did.did_type == DIDType.FILE and did.constituent: # Check if a single replica of this DID exists
|
|
541
|
+
stmt = select(
|
|
542
|
+
func.count()
|
|
543
|
+
).select_from(
|
|
544
|
+
models.RSEFileAssociation
|
|
545
|
+
).join(
|
|
546
|
+
models.RSE,
|
|
547
|
+
models.RSEFileAssociation.rse_id == models.RSE.id
|
|
548
|
+
).where(
|
|
549
|
+
and_(models.RSEFileAssociation.scope == did.scope,
|
|
550
|
+
models.RSEFileAssociation.name == did.name,
|
|
551
|
+
models.RSEFileAssociation.state == ReplicaState.AVAILABLE,
|
|
552
|
+
models.RSE.rse_type != RSEType.TAPE)
|
|
553
|
+
)
|
|
554
|
+
replica_cnt = session.execute(stmt).scalar()
|
|
555
|
+
if replica_cnt == 0: # Put the rule on the archive
|
|
556
|
+
stmt = select(
|
|
557
|
+
models.ConstituentAssociation
|
|
558
|
+
).join(
|
|
559
|
+
models.RSEFileAssociation,
|
|
560
|
+
and_(models.ConstituentAssociation.scope == models.RSEFileAssociation.scope,
|
|
561
|
+
models.ConstituentAssociation.name == models.RSEFileAssociation.name)
|
|
562
|
+
).where(
|
|
563
|
+
and_(models.ConstituentAssociation.child_scope == did.scope,
|
|
564
|
+
models.ConstituentAssociation.child_name == did.name)
|
|
565
|
+
)
|
|
566
|
+
archive = session.execute(stmt).scalars().first()
|
|
567
|
+
if archive is not None:
|
|
568
|
+
elem['name'] = archive.name
|
|
569
|
+
elem['scope'] = archive.scope
|
|
570
|
+
try:
|
|
571
|
+
stmt = select(
|
|
572
|
+
models.DataIdentifier
|
|
573
|
+
).where(
|
|
574
|
+
and_(models.DataIdentifier.scope == elem['scope'],
|
|
575
|
+
models.DataIdentifier.name == elem['name'])
|
|
576
|
+
)
|
|
577
|
+
did = session.execute(stmt).scalar_one()
|
|
578
|
+
except NoResultFound as exc:
|
|
579
|
+
raise DataIdentifierNotFound('Data identifier %s:%s is not valid.' % (elem['scope'], elem['name'])) from exc
|
|
580
|
+
except TypeError as error:
|
|
581
|
+
raise InvalidObject(error.args) from error
|
|
582
|
+
else: # Put the rule on the constituent directly
|
|
583
|
+
pass
|
|
584
|
+
|
|
585
|
+
rule_ids[(elem['scope'], elem['name'])] = []
|
|
586
|
+
|
|
587
|
+
# 3. Resolve the did into its contents
|
|
588
|
+
with METRICS.timer('add_rules.resolve_dids_to_locks_replicas'):
|
|
589
|
+
# Get all Replicas, not only the ones interesting for the rse_expression
|
|
590
|
+
datasetfiles, locks, replicas, source_replicas = __resolve_did_to_locks_and_replicas(did=did,
|
|
591
|
+
nowait=False,
|
|
592
|
+
restrict_rses=restrict_rses,
|
|
593
|
+
source_rses=all_source_rses,
|
|
594
|
+
session=session)
|
|
595
|
+
|
|
596
|
+
for rule in rules:
|
|
597
|
+
with METRICS.timer('add_rules.add_rule'):
|
|
598
|
+
# 4. Resolve the rse_expression into a list of RSE-ids
|
|
599
|
+
vo = rule['account'].vo
|
|
600
|
+
if rule.get('ignore_availability'):
|
|
601
|
+
rses = parse_expression(rule['rse_expression'], filter_={'vo': vo}, session=session)
|
|
602
|
+
else:
|
|
603
|
+
rses = parse_expression(rule['rse_expression'], filter_={'vo': vo, 'availability_write': True}, session=session)
|
|
604
|
+
|
|
605
|
+
if rule.get('lifetime', None) is None: # Check if one of the rses is a staging area
|
|
606
|
+
if [rse for rse in rses if rse.get('staging_area', False)]:
|
|
607
|
+
raise StagingAreaRuleRequiresLifetime()
|
|
608
|
+
|
|
609
|
+
# Check SCRATCHDISK Policy
|
|
610
|
+
try:
|
|
611
|
+
lifetime = get_scratch_policy(rule.get('account'), rses, rule.get('lifetime', None), session=session)
|
|
612
|
+
except UndefinedPolicy:
|
|
613
|
+
lifetime = rule.get('lifetime', None)
|
|
614
|
+
|
|
615
|
+
rule['lifetime'] = lifetime
|
|
616
|
+
|
|
617
|
+
# 4.5 Get the lifetime
|
|
618
|
+
eol_at = define_eol(did.scope, did.name, rses, session=session)
|
|
619
|
+
|
|
620
|
+
# Auto-lock rules for TAPE rses
|
|
621
|
+
if not rule.get('locked', False) and rule.get('lifetime', None) is None:
|
|
622
|
+
if [rse for rse in rses if rse.get('rse_type', RSEType.DISK) == RSEType.TAPE]:
|
|
623
|
+
rule['locked'] = True
|
|
624
|
+
|
|
625
|
+
# Block manual approval if RSE does not allow it
|
|
626
|
+
if rule.get('ask_approval', False):
|
|
627
|
+
for rse in rses:
|
|
628
|
+
if list_rse_attributes(rse_id=rse['id'], session=session).get(RseAttr.BLOCK_MANUAL_APPROVAL, False):
|
|
629
|
+
raise ManualRuleApprovalBlocked()
|
|
630
|
+
|
|
631
|
+
if rule.get('source_replica_expression'):
|
|
632
|
+
source_rses = parse_expression(rule.get('source_replica_expression'), filter_={'vo': vo}, session=session)
|
|
633
|
+
else:
|
|
634
|
+
source_rses = []
|
|
635
|
+
|
|
636
|
+
# 5. Create the RSE selector
|
|
637
|
+
with METRICS.timer('add_rules.create_rse_selector'):
|
|
638
|
+
rseselector = RSESelector(account=rule['account'], rses=rses, weight=rule.get('weight'), copies=rule['copies'], ignore_account_limit=rule.get('ask_approval', False), session=session)
|
|
639
|
+
|
|
640
|
+
# 4. Create the replication rule
|
|
641
|
+
with METRICS.timer('add_rules.create_rule'):
|
|
642
|
+
grouping = {'ALL': RuleGrouping.ALL, 'NONE': RuleGrouping.NONE}.get(str(rule.get('grouping')), RuleGrouping.DATASET)
|
|
643
|
+
|
|
644
|
+
rule_lifetime: Optional[int] = rule.get('lifetime')
|
|
645
|
+
expires_at: Optional[datetime] = datetime.utcnow() + timedelta(seconds=rule_lifetime) if rule_lifetime is not None else None
|
|
646
|
+
|
|
647
|
+
notify = {'Y': RuleNotification.YES, 'C': RuleNotification.CLOSE, 'P': RuleNotification.PROGRESS, None: RuleNotification.NO}.get(rule.get('notify'))
|
|
648
|
+
|
|
649
|
+
if rule.get('meta') is not None:
|
|
650
|
+
try:
|
|
651
|
+
meta = json.dumps(rule.get('meta'))
|
|
652
|
+
except Exception:
|
|
653
|
+
meta = None
|
|
654
|
+
else:
|
|
655
|
+
meta = None
|
|
656
|
+
|
|
657
|
+
new_rule = models.ReplicationRule(account=rule['account'],
|
|
658
|
+
name=did.name,
|
|
659
|
+
scope=did.scope,
|
|
660
|
+
did_type=did.did_type,
|
|
661
|
+
copies=rule['copies'],
|
|
662
|
+
rse_expression=rule['rse_expression'],
|
|
663
|
+
locked=rule.get('locked'),
|
|
664
|
+
grouping=grouping,
|
|
665
|
+
expires_at=expires_at,
|
|
666
|
+
weight=rule.get('weight'),
|
|
667
|
+
source_replica_expression=rule.get('source_replica_expression'),
|
|
668
|
+
activity=rule.get('activity'),
|
|
669
|
+
subscription_id=rule.get('subscription_id'),
|
|
670
|
+
notification=notify,
|
|
671
|
+
purge_replicas=rule.get('purge_replicas', False),
|
|
672
|
+
ignore_availability=rule.get('ignore_availability', False),
|
|
673
|
+
comments=rule.get('comment', None),
|
|
674
|
+
priority=rule.get('priority', 3),
|
|
675
|
+
split_container=rule.get('split_container', False),
|
|
676
|
+
meta=meta,
|
|
677
|
+
eol_at=eol_at)
|
|
678
|
+
try:
|
|
679
|
+
new_rule.save(session=session)
|
|
680
|
+
except IntegrityError as error:
|
|
681
|
+
if match('.*ORA-00001.*', str(error.args[0])):
|
|
682
|
+
raise DuplicateRule(error.args[0]) from error
|
|
683
|
+
elif str(error.args[0]) == '(IntegrityError) UNIQUE constraint failed: rules.scope, rules.name, rules.account, rules.rse_expression, rules.copies':
|
|
684
|
+
raise DuplicateRule(error.args[0]) from error
|
|
685
|
+
raise InvalidReplicationRule(error.args[0]) from error
|
|
686
|
+
|
|
687
|
+
rule_ids[(did.scope, did.name)].append(new_rule.id)
|
|
688
|
+
|
|
689
|
+
if rule.get('ask_approval', False):
|
|
690
|
+
new_rule.state = RuleState.WAITING_APPROVAL
|
|
691
|
+
# Block manual approval for multi-rse rules
|
|
692
|
+
if len(rses) > 1:
|
|
693
|
+
raise InvalidReplicationRule('Ask approval is not allowed for rules with multiple RSEs')
|
|
694
|
+
if len(rses) == 1 and not did.is_open and did.bytes is not None and did.length is not None:
|
|
695
|
+
# This rule can be considered for auto-approval:
|
|
696
|
+
rse_attr = list_rse_attributes(rse_id=rses[0]['id'], session=session)
|
|
697
|
+
auto_approve = False
|
|
698
|
+
if RseAttr.AUTO_APPROVE_BYTES in rse_attr and RseAttr.AUTO_APPROVE_FILES in rse_attr:
|
|
699
|
+
if did.bytes < int(rse_attr.get(RseAttr.AUTO_APPROVE_BYTES)) and did.length < int(rse_attr.get(RseAttr.AUTO_APPROVE_BYTES)):
|
|
700
|
+
auto_approve = True
|
|
701
|
+
elif did.bytes < int(rse_attr.get(RseAttr.AUTO_APPROVE_BYTES, -1)):
|
|
702
|
+
auto_approve = True
|
|
703
|
+
elif did.length < int(rse_attr.get(RseAttr.AUTO_APPROVE_FILES, -1)):
|
|
704
|
+
auto_approve = True
|
|
705
|
+
if auto_approve:
|
|
706
|
+
logger(logging.DEBUG, "Auto approving rule %s", str(new_rule.id))
|
|
707
|
+
logger(logging.DEBUG, "Created rule %s for injection", str(new_rule.id))
|
|
708
|
+
approve_rule(rule_id=new_rule.id, notify_approvers=False, session=session)
|
|
709
|
+
continue
|
|
710
|
+
logger(logging.DEBUG, "Created rule %s in waiting for approval", str(new_rule.id))
|
|
711
|
+
__create_rule_approval_email(rule=new_rule, session=session)
|
|
712
|
+
continue
|
|
713
|
+
|
|
714
|
+
delay_injection = rule.get('delay_injection')
|
|
715
|
+
if rule.get('asynchronous', False) or delay_injection:
|
|
716
|
+
new_rule.state = RuleState.INJECT
|
|
717
|
+
logger(logging.DEBUG, "Created rule %s for injection", str(new_rule.id))
|
|
718
|
+
if delay_injection:
|
|
719
|
+
new_rule.created_at = datetime.utcnow() + timedelta(seconds=delay_injection)
|
|
720
|
+
logger(logging.DEBUG, "Scheduled rule %s for injection on %s", (str(new_rule.id), new_rule.created_at))
|
|
721
|
+
continue
|
|
722
|
+
|
|
723
|
+
if rule.get('split_container', False) and did.did_type == DIDType.CONTAINER:
|
|
724
|
+
new_rule.state = RuleState.INJECT
|
|
725
|
+
logger(logging.DEBUG, "Created rule %s for injection due to Split Container mode", str(new_rule.id))
|
|
726
|
+
continue
|
|
727
|
+
|
|
728
|
+
# 5. Apply the replication rule to create locks, replicas and transfers
|
|
729
|
+
with METRICS.timer('add_rules.create_locks_replicas_transfers'):
|
|
730
|
+
try:
|
|
731
|
+
__create_locks_replicas_transfers(datasetfiles=datasetfiles,
|
|
732
|
+
locks=locks,
|
|
733
|
+
replicas=replicas,
|
|
734
|
+
source_replicas=source_replicas,
|
|
735
|
+
rseselector=rseselector,
|
|
736
|
+
rule=new_rule,
|
|
737
|
+
preferred_rse_ids=[],
|
|
738
|
+
source_rses=[rse['id'] for rse in source_rses],
|
|
739
|
+
session=session)
|
|
740
|
+
except IntegrityError as error:
|
|
741
|
+
raise ReplicationRuleCreationTemporaryFailed(error.args[0]) from error
|
|
742
|
+
|
|
743
|
+
if new_rule.locks_stuck_cnt > 0:
|
|
744
|
+
new_rule.state = RuleState.STUCK
|
|
745
|
+
new_rule.error = 'MissingSourceReplica'
|
|
746
|
+
if new_rule.grouping != RuleGrouping.NONE:
|
|
747
|
+
stmt = update(
|
|
748
|
+
models.DatasetLock
|
|
749
|
+
).where(
|
|
750
|
+
models.DatasetLock.rule_id == new_rule.id
|
|
751
|
+
).values({
|
|
752
|
+
models.DatasetLock.state: LockState.STUCK
|
|
753
|
+
})
|
|
754
|
+
session.execute(stmt)
|
|
755
|
+
elif new_rule.locks_replicating_cnt == 0:
|
|
756
|
+
new_rule.state = RuleState.OK
|
|
757
|
+
if new_rule.grouping != RuleGrouping.NONE:
|
|
758
|
+
stmt = update(
|
|
759
|
+
models.DatasetLock
|
|
760
|
+
).where(
|
|
761
|
+
models.DatasetLock.rule_id == new_rule.id
|
|
762
|
+
).values({
|
|
763
|
+
models.DatasetLock.state: LockState.OK
|
|
764
|
+
})
|
|
765
|
+
session.execute(stmt)
|
|
766
|
+
session.flush()
|
|
767
|
+
if new_rule.notification == RuleNotification.YES:
|
|
768
|
+
generate_email_for_rule_ok_notification(rule=new_rule, session=session)
|
|
769
|
+
generate_rule_notifications(rule=new_rule, replicating_locks_before=0, session=session)
|
|
770
|
+
else:
|
|
771
|
+
new_rule.state = RuleState.REPLICATING
|
|
772
|
+
if new_rule.grouping != RuleGrouping.NONE:
|
|
773
|
+
stmt = update(
|
|
774
|
+
models.DatasetLock
|
|
775
|
+
).where(
|
|
776
|
+
models.DatasetLock.rule_id == new_rule.id
|
|
777
|
+
).values({
|
|
778
|
+
models.DatasetLock.state: LockState.REPLICATING
|
|
779
|
+
})
|
|
780
|
+
session.execute(stmt)
|
|
781
|
+
|
|
782
|
+
# Add rule to History
|
|
783
|
+
insert_rule_history(rule=new_rule, recent=True, longterm=True, session=session)
|
|
784
|
+
|
|
785
|
+
logger(logging.INFO, "Created rule %s [%d/%d/%d] in state %s", str(new_rule.id), new_rule.locks_ok_cnt, new_rule.locks_replicating_cnt, new_rule.locks_stuck_cnt, str(new_rule.state))
|
|
786
|
+
|
|
787
|
+
return rule_ids
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
@transactional_session
|
|
791
|
+
def inject_rule(
|
|
792
|
+
rule_id: str,
|
|
793
|
+
*,
|
|
794
|
+
session: "Session",
|
|
795
|
+
logger: LoggerFunction = logging.log
|
|
796
|
+
) -> None:
|
|
797
|
+
"""
|
|
798
|
+
Inject a replication rule.
|
|
799
|
+
|
|
800
|
+
:param rule_id: The id of the rule to inject.
|
|
801
|
+
:param new_owner: The new owner of the rule.
|
|
802
|
+
:param session: The database session in use.
|
|
803
|
+
:param logger: Optional decorated logger that can be passed from the calling daemons or servers.
|
|
804
|
+
:raises: InvalidReplicationRule, InsufficientAccountLimit, InvalidRSEExpression, DataId, RSEOverQuota
|
|
805
|
+
"""
|
|
806
|
+
|
|
807
|
+
try:
|
|
808
|
+
stmt = select(
|
|
809
|
+
models.ReplicationRule
|
|
810
|
+
).where(
|
|
811
|
+
models.ReplicationRule.id == rule_id
|
|
812
|
+
).with_for_update(
|
|
813
|
+
nowait=True
|
|
814
|
+
)
|
|
815
|
+
rule = session.execute(stmt).scalar_one()
|
|
816
|
+
except NoResultFound as exc:
|
|
817
|
+
raise RuleNotFound('No rule with the id %s found' % (rule_id)) from exc
|
|
818
|
+
|
|
819
|
+
# Check if rule will expire in the next 5 minutes:
|
|
820
|
+
if rule.child_rule_id is None and rule.expires_at is not None and rule.expires_at < datetime.utcnow() + timedelta(seconds=300):
|
|
821
|
+
logger(logging.INFO, 'Rule %s expiring soon, skipping', str(rule.id))
|
|
822
|
+
return
|
|
823
|
+
|
|
824
|
+
# Special R2D2 container handling
|
|
825
|
+
if (rule.did_type == DIDType.CONTAINER and '.r2d2_request.' in rule.name) or (rule.split_container and rule.did_type == DIDType.CONTAINER):
|
|
826
|
+
logger(logging.DEBUG, "Creating dataset rules for Split Container rule %s", str(rule.id))
|
|
827
|
+
# Get all child datasets and put rules on them
|
|
828
|
+
dids = [{'scope': dataset['scope'], 'name': dataset['name']} for dataset in rucio.core.did.list_child_datasets(scope=rule.scope, name=rule.name, session=session)]
|
|
829
|
+
# Remove duplicates from the list of dictionaries
|
|
830
|
+
dids = [dict(t) for t in {tuple(d.items()) for d in dids}]
|
|
831
|
+
# Remove dids which already have a similar rule
|
|
832
|
+
stmt = select(
|
|
833
|
+
models.ReplicationRule.id
|
|
834
|
+
).where(
|
|
835
|
+
and_(models.ReplicationRule.account == rule.account,
|
|
836
|
+
models.ReplicationRule.rse_expression == rule.rse_expression)
|
|
837
|
+
)
|
|
838
|
+
dids = [did for did in dids if session.execute(stmt.where(and_(models.ReplicationRule.scope == did['scope'], models.ReplicationRule.name == did['name']))).scalar_one_or_none() is None]
|
|
839
|
+
if rule.expires_at:
|
|
840
|
+
lifetime = (rule.expires_at - datetime.utcnow()).days * 24 * 3600 + (rule.expires_at - datetime.utcnow()).seconds
|
|
841
|
+
else:
|
|
842
|
+
lifetime = None
|
|
843
|
+
|
|
844
|
+
notify = {RuleNotification.YES: 'Y', RuleNotification.CLOSE: 'C', RuleNotification.PROGRESS: 'P'}.get(rule.notification, 'N')
|
|
845
|
+
|
|
846
|
+
add_rule(dids=dids,
|
|
847
|
+
account=rule.account,
|
|
848
|
+
copies=rule.copies,
|
|
849
|
+
rse_expression=rule.rse_expression,
|
|
850
|
+
grouping='DATASET',
|
|
851
|
+
weight=None,
|
|
852
|
+
lifetime=lifetime,
|
|
853
|
+
locked=False,
|
|
854
|
+
subscription_id=None,
|
|
855
|
+
activity=rule.activity,
|
|
856
|
+
notify=notify,
|
|
857
|
+
comment=rule.comments,
|
|
858
|
+
asynchronous=True,
|
|
859
|
+
ignore_availability=rule.ignore_availability,
|
|
860
|
+
ignore_account_limit=True,
|
|
861
|
+
priority=rule.priority,
|
|
862
|
+
split_container=rule.split_container,
|
|
863
|
+
session=session)
|
|
864
|
+
rule.delete(session=session)
|
|
865
|
+
return
|
|
866
|
+
|
|
867
|
+
# 1. Resolve the rse_expression into a list of RSE-ids
|
|
868
|
+
with METRICS.timer('inject_rule.parse_rse_expression'):
|
|
869
|
+
vo = rule['account'].vo
|
|
870
|
+
if rule.ignore_availability:
|
|
871
|
+
rses = parse_expression(rule.rse_expression, filter_={'vo': vo}, session=session)
|
|
872
|
+
else:
|
|
873
|
+
rses = parse_expression(rule.rse_expression, filter_={'vo': vo, 'availability_write': True}, session=session)
|
|
874
|
+
|
|
875
|
+
if rule.source_replica_expression:
|
|
876
|
+
source_rses = parse_expression(rule.source_replica_expression, filter_={'vo': vo}, session=session)
|
|
877
|
+
else:
|
|
878
|
+
source_rses = []
|
|
879
|
+
|
|
880
|
+
# 2. Create the rse selector
|
|
881
|
+
with METRICS.timer('inject_rule.create_rse_selector'):
|
|
882
|
+
rseselector = RSESelector(account=rule['account'], rses=rses, weight=rule.weight, copies=rule.copies, ignore_account_limit=rule.ignore_account_limit, session=session)
|
|
883
|
+
|
|
884
|
+
# 3. Get the did
|
|
885
|
+
with METRICS.timer('inject_rule.get_did'):
|
|
886
|
+
try:
|
|
887
|
+
stmt = select(
|
|
888
|
+
models.DataIdentifier
|
|
889
|
+
).where(
|
|
890
|
+
and_(models.DataIdentifier.scope == rule.scope,
|
|
891
|
+
models.DataIdentifier.name == rule.name)
|
|
892
|
+
)
|
|
893
|
+
did = session.execute(stmt).scalar_one()
|
|
894
|
+
except NoResultFound as exc:
|
|
895
|
+
raise DataIdentifierNotFound('Data identifier %s:%s is not valid.' % (rule.scope, rule.name)) from exc
|
|
896
|
+
except TypeError as error:
|
|
897
|
+
raise InvalidObject(error.args) from error
|
|
898
|
+
|
|
899
|
+
# 4. Apply the rule
|
|
900
|
+
with METRICS.timer('inject_rule.apply_rule'):
|
|
901
|
+
try:
|
|
902
|
+
apply_rule(did, rule, [x['id'] for x in rses], [x['id'] for x in source_rses], rseselector, session=session)
|
|
903
|
+
except IntegrityError as error:
|
|
904
|
+
raise ReplicationRuleCreationTemporaryFailed(error.args[0]) from error
|
|
905
|
+
|
|
906
|
+
if rule.locks_stuck_cnt > 0:
|
|
907
|
+
rule.state = RuleState.STUCK
|
|
908
|
+
rule.error = 'MissingSourceReplica'
|
|
909
|
+
if rule.grouping != RuleGrouping.NONE:
|
|
910
|
+
stmt = update(
|
|
911
|
+
models.DatasetLock
|
|
912
|
+
).where(
|
|
913
|
+
models.DatasetLock.rule_id == rule.id
|
|
914
|
+
).values({
|
|
915
|
+
models.DatasetLock.state: LockState.STUCK
|
|
916
|
+
})
|
|
917
|
+
session.execute(stmt)
|
|
918
|
+
elif rule.locks_replicating_cnt == 0:
|
|
919
|
+
rule.state = RuleState.OK
|
|
920
|
+
if rule.grouping != RuleGrouping.NONE:
|
|
921
|
+
stmt = update(
|
|
922
|
+
models.DatasetLock
|
|
923
|
+
).where(
|
|
924
|
+
models.DatasetLock.rule_id == rule.id
|
|
925
|
+
).values({
|
|
926
|
+
models.DatasetLock.state: LockState.OK
|
|
927
|
+
})
|
|
928
|
+
session.execute(stmt)
|
|
929
|
+
session.flush()
|
|
930
|
+
if rule.notification == RuleNotification.YES:
|
|
931
|
+
generate_email_for_rule_ok_notification(rule=rule, session=session)
|
|
932
|
+
generate_rule_notifications(rule=rule, replicating_locks_before=0, session=session)
|
|
933
|
+
# Try to release potential parent rules
|
|
934
|
+
release_parent_rule(child_rule_id=rule.id, session=session)
|
|
935
|
+
else:
|
|
936
|
+
rule.state = RuleState.REPLICATING
|
|
937
|
+
if rule.grouping != RuleGrouping.NONE:
|
|
938
|
+
stmt = update(
|
|
939
|
+
models.DatasetLock
|
|
940
|
+
).where(
|
|
941
|
+
models.DatasetLock.rule_id == rule.id
|
|
942
|
+
).values({
|
|
943
|
+
models.DatasetLock.state: LockState.REPLICATING
|
|
944
|
+
})
|
|
945
|
+
session.execute(stmt)
|
|
946
|
+
|
|
947
|
+
# Add rule to History
|
|
948
|
+
insert_rule_history(rule=rule, recent=True, longterm=True, session=session)
|
|
949
|
+
|
|
950
|
+
logger(logging.INFO, "Created rule %s [%d/%d/%d] with new algorithm for did %s:%s in state %s", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt, rule.scope, rule.name, str(rule.state))
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
@stream_session
|
|
954
|
+
def list_rules(
|
|
955
|
+
filters: Optional[dict[str, Any]] = None,
|
|
956
|
+
*,
|
|
957
|
+
session: "Session"
|
|
958
|
+
) -> 'Iterator[dict[str, Any]]':
|
|
959
|
+
"""
|
|
960
|
+
List replication rules.
|
|
961
|
+
|
|
962
|
+
:param filters: dictionary of attributes by which the results should be filtered.
|
|
963
|
+
:param session: The database session in use.
|
|
964
|
+
:raises: RucioException
|
|
965
|
+
"""
|
|
966
|
+
|
|
967
|
+
stmt = select(
|
|
968
|
+
models.ReplicationRule,
|
|
969
|
+
models.DataIdentifier.bytes
|
|
970
|
+
).join(
|
|
971
|
+
models.DataIdentifier,
|
|
972
|
+
and_(
|
|
973
|
+
models.ReplicationRule.scope == models.DataIdentifier.scope,
|
|
974
|
+
models.ReplicationRule.name == models.DataIdentifier.name
|
|
975
|
+
)
|
|
976
|
+
)
|
|
977
|
+
if filters is not None:
|
|
978
|
+
for (key, value) in filters.items():
|
|
979
|
+
if key in ['account', 'scope']:
|
|
980
|
+
if '*' in value.internal:
|
|
981
|
+
value = value.internal.replace('*', '%')
|
|
982
|
+
stmt = stmt.where(getattr(models.ReplicationRule, key).like(value))
|
|
983
|
+
continue
|
|
984
|
+
# else fall through
|
|
985
|
+
elif key == 'created_before':
|
|
986
|
+
stmt = stmt.where(models.ReplicationRule.created_at <= str_to_date(value))
|
|
987
|
+
continue
|
|
988
|
+
elif key == 'created_after':
|
|
989
|
+
stmt = stmt.where(models.ReplicationRule.created_at >= str_to_date(value))
|
|
990
|
+
continue
|
|
991
|
+
elif key == 'updated_before':
|
|
992
|
+
stmt = stmt.where(models.ReplicationRule.updated_at <= str_to_date(value))
|
|
993
|
+
continue
|
|
994
|
+
elif key == 'updated_after':
|
|
995
|
+
stmt = stmt.where(models.ReplicationRule.updated_at >= str_to_date(value))
|
|
996
|
+
continue
|
|
997
|
+
elif key == 'state':
|
|
998
|
+
if isinstance(value, str):
|
|
999
|
+
value = RuleState(value)
|
|
1000
|
+
else:
|
|
1001
|
+
try:
|
|
1002
|
+
value = RuleState[value]
|
|
1003
|
+
except ValueError:
|
|
1004
|
+
pass
|
|
1005
|
+
elif key == 'did_type' and isinstance(value, str):
|
|
1006
|
+
value = DIDType(value)
|
|
1007
|
+
elif key == 'grouping' and isinstance(value, str):
|
|
1008
|
+
value = RuleGrouping(value)
|
|
1009
|
+
stmt = stmt.where(getattr(models.ReplicationRule, key) == value)
|
|
1010
|
+
|
|
1011
|
+
try:
|
|
1012
|
+
for rule, data_identifier_bytes in session.execute(stmt).yield_per(5):
|
|
1013
|
+
d = rule.to_dict()
|
|
1014
|
+
d['bytes'] = data_identifier_bytes
|
|
1015
|
+
yield d
|
|
1016
|
+
except StatementError as exc:
|
|
1017
|
+
raise RucioException('Badly formatted input (IDs?)') from exc
|
|
1018
|
+
|
|
1019
|
+
|
|
1020
|
+
@stream_session
|
|
1021
|
+
def list_rule_history(
|
|
1022
|
+
rule_id: str,
|
|
1023
|
+
*,
|
|
1024
|
+
session: "Session"
|
|
1025
|
+
) -> 'Iterator[dict[str, Any]]':
|
|
1026
|
+
"""
|
|
1027
|
+
List the rule history of a rule.
|
|
1028
|
+
|
|
1029
|
+
:param rule_id: The id of the rule.
|
|
1030
|
+
:param session: The database session in use.
|
|
1031
|
+
:raises: RucioException
|
|
1032
|
+
"""
|
|
1033
|
+
|
|
1034
|
+
stmt = select(
|
|
1035
|
+
models.ReplicationRuleHistory.updated_at,
|
|
1036
|
+
models.ReplicationRuleHistory.state,
|
|
1037
|
+
models.ReplicationRuleHistory.locks_ok_cnt,
|
|
1038
|
+
models.ReplicationRuleHistory.locks_stuck_cnt,
|
|
1039
|
+
models.ReplicationRuleHistory.locks_replicating_cnt
|
|
1040
|
+
).where(
|
|
1041
|
+
models.ReplicationRuleHistory.id == rule_id
|
|
1042
|
+
).order_by(
|
|
1043
|
+
models.ReplicationRuleHistory.updated_at
|
|
1044
|
+
)
|
|
1045
|
+
|
|
1046
|
+
try:
|
|
1047
|
+
for rule in session.execute(stmt).yield_per(5):
|
|
1048
|
+
yield rule._asdict()
|
|
1049
|
+
except StatementError as exc:
|
|
1050
|
+
raise RucioException('Badly formatted input (IDs?)') from exc
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
@stream_session
|
|
1054
|
+
def list_rule_full_history(
|
|
1055
|
+
scope: InternalScope,
|
|
1056
|
+
name: str,
|
|
1057
|
+
*,
|
|
1058
|
+
session: "Session"
|
|
1059
|
+
) -> 'Iterator[dict[str, Any]]':
|
|
1060
|
+
"""
|
|
1061
|
+
List the rule history of a DID.
|
|
1062
|
+
|
|
1063
|
+
:param scope: The scope of the DID.
|
|
1064
|
+
:param name: The name of the DID.
|
|
1065
|
+
:param session: The database session in use.
|
|
1066
|
+
:raises: RucioException
|
|
1067
|
+
"""
|
|
1068
|
+
|
|
1069
|
+
stmt = select(
|
|
1070
|
+
models.ReplicationRuleHistory.id.label('rule_id'),
|
|
1071
|
+
models.ReplicationRuleHistory.created_at,
|
|
1072
|
+
models.ReplicationRuleHistory.updated_at,
|
|
1073
|
+
models.ReplicationRuleHistory.rse_expression,
|
|
1074
|
+
models.ReplicationRuleHistory.state,
|
|
1075
|
+
models.ReplicationRuleHistory.account,
|
|
1076
|
+
models.ReplicationRuleHistory.locks_ok_cnt,
|
|
1077
|
+
models.ReplicationRuleHistory.locks_stuck_cnt,
|
|
1078
|
+
models.ReplicationRuleHistory.locks_replicating_cnt
|
|
1079
|
+
).with_hint(
|
|
1080
|
+
models.ReplicationRuleHistory, 'INDEX(RULES_HISTORY_SCOPENAME_IDX)', 'oracle'
|
|
1081
|
+
).where(
|
|
1082
|
+
and_(models.ReplicationRuleHistory.scope == scope,
|
|
1083
|
+
models.ReplicationRuleHistory.name == name)
|
|
1084
|
+
).order_by(
|
|
1085
|
+
models.ReplicationRuleHistory.created_at,
|
|
1086
|
+
models.ReplicationRuleHistory.updated_at
|
|
1087
|
+
)
|
|
1088
|
+
for rule in session.execute(stmt).yield_per(5):
|
|
1089
|
+
yield rule._asdict()
|
|
1090
|
+
|
|
1091
|
+
|
|
1092
|
+
@stream_session
|
|
1093
|
+
def list_associated_rules_for_file(
|
|
1094
|
+
scope: InternalScope,
|
|
1095
|
+
name: str,
|
|
1096
|
+
*,
|
|
1097
|
+
session: "Session"
|
|
1098
|
+
) -> 'Iterator[dict[str, Any]]':
|
|
1099
|
+
"""
|
|
1100
|
+
List replication rules a file is affected from.
|
|
1101
|
+
|
|
1102
|
+
:param scope: Scope of the file.
|
|
1103
|
+
:param name: Name of the file.
|
|
1104
|
+
:param session: The database session in use.
|
|
1105
|
+
:raises: RucioException
|
|
1106
|
+
"""
|
|
1107
|
+
rucio.core.did.get_did(scope=scope, name=name, session=session) # Check if the did actually exists
|
|
1108
|
+
stmt = select(
|
|
1109
|
+
models.ReplicationRule,
|
|
1110
|
+
models.DataIdentifier.bytes
|
|
1111
|
+
).distinct(
|
|
1112
|
+
).join(
|
|
1113
|
+
models.ReplicaLock,
|
|
1114
|
+
models.ReplicationRule.id == models.ReplicaLock.rule_id
|
|
1115
|
+
).join(
|
|
1116
|
+
models.DataIdentifier,
|
|
1117
|
+
and_(models.ReplicationRule.scope == models.DataIdentifier.scope,
|
|
1118
|
+
models.ReplicationRule.name == models.DataIdentifier.name)
|
|
1119
|
+
).with_hint(
|
|
1120
|
+
models.ReplicaLock, 'INDEX(LOCKS LOCKS_PK)', 'oracle'
|
|
1121
|
+
).where(
|
|
1122
|
+
and_(models.ReplicaLock.scope == scope,
|
|
1123
|
+
models.ReplicaLock.name == name)
|
|
1124
|
+
)
|
|
1125
|
+
try:
|
|
1126
|
+
for rule, data_identifier_bytes in session.execute(stmt).yield_per(5):
|
|
1127
|
+
d = rule.to_dict()
|
|
1128
|
+
d['bytes'] = data_identifier_bytes
|
|
1129
|
+
yield d
|
|
1130
|
+
except StatementError as exc:
|
|
1131
|
+
raise RucioException('Badly formatted input (IDs?)') from exc
|
|
1132
|
+
|
|
1133
|
+
|
|
1134
|
+
@transactional_session
|
|
1135
|
+
def delete_rule(
|
|
1136
|
+
rule_id: str,
|
|
1137
|
+
purge_replicas: Optional[bool] = None,
|
|
1138
|
+
soft: bool = False,
|
|
1139
|
+
delete_parent: bool = False,
|
|
1140
|
+
nowait: bool = False,
|
|
1141
|
+
*,
|
|
1142
|
+
session: "Session",
|
|
1143
|
+
ignore_rule_lock: bool = False
|
|
1144
|
+
) -> None:
|
|
1145
|
+
"""
|
|
1146
|
+
Delete a replication rule.
|
|
1147
|
+
|
|
1148
|
+
:param rule_id: The rule to delete.
|
|
1149
|
+
:param purge_replicas: Purge the replicas immediately.
|
|
1150
|
+
:param soft: Only perform a soft deletion.
|
|
1151
|
+
:param delete_parent: Delete rules even if they have a child_rule_id set.
|
|
1152
|
+
:param nowait: Nowait parameter for the FOR UPDATE statement.
|
|
1153
|
+
:param session: The database session in use.
|
|
1154
|
+
:param ignore_rule_lock: Ignore any locks on the rule
|
|
1155
|
+
:raises: RuleNotFound if no Rule can be found.
|
|
1156
|
+
:raises: UnsupportedOperation if the Rule is locked.
|
|
1157
|
+
"""
|
|
1158
|
+
|
|
1159
|
+
with METRICS.timer('delete_rule.total'):
|
|
1160
|
+
try:
|
|
1161
|
+
stmt = select(
|
|
1162
|
+
models.ReplicationRule
|
|
1163
|
+
).where(
|
|
1164
|
+
models.ReplicationRule.id == rule_id
|
|
1165
|
+
).with_for_update(
|
|
1166
|
+
nowait=nowait
|
|
1167
|
+
)
|
|
1168
|
+
rule = session.execute(stmt).scalar_one()
|
|
1169
|
+
except NoResultFound as exc:
|
|
1170
|
+
raise RuleNotFound('No rule with the id %s found' % rule_id) from exc
|
|
1171
|
+
if rule.locked and not ignore_rule_lock:
|
|
1172
|
+
raise UnsupportedOperation('The replication rule is locked and has to be unlocked before it can be deleted.')
|
|
1173
|
+
|
|
1174
|
+
if rule.child_rule_id is not None and not delete_parent:
|
|
1175
|
+
raise UnsupportedOperation('The replication rule has a child rule and thus cannot be deleted.')
|
|
1176
|
+
|
|
1177
|
+
if purge_replicas is not None:
|
|
1178
|
+
rule.purge_replicas = purge_replicas
|
|
1179
|
+
|
|
1180
|
+
if soft:
|
|
1181
|
+
if rule.expires_at:
|
|
1182
|
+
rule.expires_at = min(datetime.utcnow() + timedelta(seconds=3600), rule.expires_at)
|
|
1183
|
+
else:
|
|
1184
|
+
rule.expires_at = datetime.utcnow() + timedelta(seconds=3600)
|
|
1185
|
+
if rule.child_rule_id is not None and delete_parent:
|
|
1186
|
+
rule.child_rule_id = None
|
|
1187
|
+
insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
|
|
1188
|
+
return
|
|
1189
|
+
|
|
1190
|
+
stmt = select(
|
|
1191
|
+
models.ReplicaLock
|
|
1192
|
+
).where(
|
|
1193
|
+
models.ReplicaLock.rule_id == rule_id
|
|
1194
|
+
).with_for_update(
|
|
1195
|
+
nowait=nowait
|
|
1196
|
+
)
|
|
1197
|
+
results = session.execute(stmt).yield_per(100)
|
|
1198
|
+
|
|
1199
|
+
# Remove locks, set tombstone if applicable
|
|
1200
|
+
transfers_to_delete = [] # [{'scope': , 'name':, 'rse_id':}]
|
|
1201
|
+
account_counter_decreases = {} # {'rse_id': [file_size, file_size, file_size]}
|
|
1202
|
+
|
|
1203
|
+
for result in results:
|
|
1204
|
+
lock = result[0]
|
|
1205
|
+
if __delete_lock_and_update_replica(lock=lock, purge_replicas=rule.purge_replicas,
|
|
1206
|
+
nowait=nowait, session=session):
|
|
1207
|
+
transfers_to_delete.append({'scope': lock.scope, 'name': lock.name, 'rse_id': lock.rse_id})
|
|
1208
|
+
if lock.rse_id not in account_counter_decreases:
|
|
1209
|
+
account_counter_decreases[lock.rse_id] = []
|
|
1210
|
+
account_counter_decreases[lock.rse_id].append(lock.bytes)
|
|
1211
|
+
|
|
1212
|
+
# Delete the DatasetLocks
|
|
1213
|
+
stmt = delete(
|
|
1214
|
+
models.DatasetLock
|
|
1215
|
+
).where(
|
|
1216
|
+
models.DatasetLock.rule_id == rule_id
|
|
1217
|
+
).execution_options(
|
|
1218
|
+
synchronize_session=False
|
|
1219
|
+
)
|
|
1220
|
+
session.execute(stmt)
|
|
1221
|
+
|
|
1222
|
+
# Decrease account_counters
|
|
1223
|
+
for rse_id in account_counter_decreases.keys():
|
|
1224
|
+
account_counter.decrease(rse_id=rse_id, account=rule.account, files=len(account_counter_decreases[rse_id]),
|
|
1225
|
+
bytes_=sum(account_counter_decreases[rse_id]), session=session)
|
|
1226
|
+
|
|
1227
|
+
# Try to release potential parent rules
|
|
1228
|
+
release_parent_rule(child_rule_id=rule.id, remove_parent_expiration=True, session=session)
|
|
1229
|
+
|
|
1230
|
+
# Insert history
|
|
1231
|
+
insert_rule_history(rule=rule, recent=False, longterm=True, session=session)
|
|
1232
|
+
|
|
1233
|
+
session.flush()
|
|
1234
|
+
rule.delete(session=session)
|
|
1235
|
+
|
|
1236
|
+
for transfer in transfers_to_delete:
|
|
1237
|
+
transfers_to_cancel = request_core.cancel_request_did(scope=transfer['scope'], name=transfer['name'],
|
|
1238
|
+
dest_rse_id=transfer['rse_id'], session=session)
|
|
1239
|
+
transfer_core.cancel_transfers(transfers_to_cancel)
|
|
1240
|
+
|
|
1241
|
+
|
|
1242
|
+
@transactional_session
|
|
1243
|
+
def repair_rule(
|
|
1244
|
+
rule_id: str,
|
|
1245
|
+
*,
|
|
1246
|
+
session: "Session",
|
|
1247
|
+
logger: LoggerFunction = logging.log
|
|
1248
|
+
) -> None:
|
|
1249
|
+
"""
|
|
1250
|
+
Repair a STUCK replication rule.
|
|
1251
|
+
|
|
1252
|
+
:param rule_id: The rule to repair.
|
|
1253
|
+
:param session: The database session in use.
|
|
1254
|
+
:param logger: Optional decorated logger that can be passed from the calling daemons or servers.
|
|
1255
|
+
"""
|
|
1256
|
+
|
|
1257
|
+
# Rule error cases:
|
|
1258
|
+
# (A) A rule gets an exception on rule-creation. This can only be the MissingSourceReplica exception.
|
|
1259
|
+
# (B) A rule gets an error when re-evaluated: InvalidRSEExpression, InvalidRuleWeight, InsufficientTargetRSEs, RSEWriteBlocked
|
|
1260
|
+
# InsufficientAccountLimit. The re-evaluation has to be done again and potential missing locks have to be
|
|
1261
|
+
# created.
|
|
1262
|
+
# (C) Transfers fail and mark locks (and the rule) as STUCK. All STUCK locks have to be repaired.
|
|
1263
|
+
# (D) Files are declared as BAD.
|
|
1264
|
+
|
|
1265
|
+
# start_time = time.time()
|
|
1266
|
+
try:
|
|
1267
|
+
stmt = select(
|
|
1268
|
+
models.ReplicationRule
|
|
1269
|
+
).where(
|
|
1270
|
+
models.ReplicationRule.id == rule_id
|
|
1271
|
+
).with_for_update(
|
|
1272
|
+
nowait=True
|
|
1273
|
+
)
|
|
1274
|
+
rule = session.execute(stmt).scalar_one()
|
|
1275
|
+
rule.updated_at = datetime.utcnow()
|
|
1276
|
+
|
|
1277
|
+
# Check if rule is longer than 2 weeks in STUCK
|
|
1278
|
+
if rule.stuck_at is None:
|
|
1279
|
+
rule.stuck_at = datetime.utcnow()
|
|
1280
|
+
if rule.stuck_at < (datetime.utcnow() - timedelta(days=14)):
|
|
1281
|
+
rule.state = RuleState.SUSPENDED
|
|
1282
|
+
insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
|
|
1283
|
+
logger(logging.INFO, 'Replication rule %s has been SUSPENDED', rule_id)
|
|
1284
|
+
return
|
|
1285
|
+
|
|
1286
|
+
# Evaluate the RSE expression to see if there is an alternative RSE anyway
|
|
1287
|
+
try:
|
|
1288
|
+
vo = rule.account.vo
|
|
1289
|
+
rses = parse_expression(rule.rse_expression, filter_={'vo': vo}, session=session)
|
|
1290
|
+
if rule.ignore_availability:
|
|
1291
|
+
target_rses = parse_expression(rule.rse_expression, filter_={'vo': vo}, session=session)
|
|
1292
|
+
else:
|
|
1293
|
+
target_rses = parse_expression(rule.rse_expression, filter_={'vo': vo, 'availability_write': True}, session=session)
|
|
1294
|
+
if rule.source_replica_expression:
|
|
1295
|
+
source_rses = parse_expression(rule.source_replica_expression, filter_={'vo': vo}, session=session)
|
|
1296
|
+
else:
|
|
1297
|
+
source_rses = []
|
|
1298
|
+
except (InvalidRSEExpression, RSEWriteBlocked) as error:
|
|
1299
|
+
rule.state = RuleState.STUCK
|
|
1300
|
+
rule.error = (str(error)[:245] + '...') if len(str(error)) > 245 else str(error)
|
|
1301
|
+
rule.save(session=session)
|
|
1302
|
+
# Insert rule history
|
|
1303
|
+
insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
|
|
1304
|
+
# Try to update the DatasetLocks
|
|
1305
|
+
if rule.grouping != RuleGrouping.NONE:
|
|
1306
|
+
stmt = update(
|
|
1307
|
+
models.DatasetLock
|
|
1308
|
+
).where(
|
|
1309
|
+
models.DatasetLock.rule_id == rule.id
|
|
1310
|
+
).values({
|
|
1311
|
+
models.DatasetLock.state: LockState.STUCK
|
|
1312
|
+
})
|
|
1313
|
+
session.execute(stmt)
|
|
1314
|
+
logger(logging.DEBUG, '%s while repairing rule %s', str(error), rule_id)
|
|
1315
|
+
return
|
|
1316
|
+
|
|
1317
|
+
# Create the RSESelector
|
|
1318
|
+
try:
|
|
1319
|
+
rseselector = RSESelector(account=rule.account,
|
|
1320
|
+
rses=target_rses,
|
|
1321
|
+
weight=rule.weight,
|
|
1322
|
+
copies=rule.copies,
|
|
1323
|
+
ignore_account_limit=rule.ignore_account_limit,
|
|
1324
|
+
session=session)
|
|
1325
|
+
except (InvalidRuleWeight, InsufficientTargetRSEs, InsufficientAccountLimit) as error:
|
|
1326
|
+
rule.state = RuleState.STUCK
|
|
1327
|
+
rule.error = (str(error)[:245] + '...') if len(str(error)) > 245 else str(error)
|
|
1328
|
+
rule.save(session=session)
|
|
1329
|
+
# Insert rule history
|
|
1330
|
+
insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
|
|
1331
|
+
# Try to update the DatasetLocks
|
|
1332
|
+
if rule.grouping != RuleGrouping.NONE:
|
|
1333
|
+
stmt = update(
|
|
1334
|
+
models.DatasetLock
|
|
1335
|
+
).where(
|
|
1336
|
+
models.DatasetLock.rule_id == rule.id
|
|
1337
|
+
).values({
|
|
1338
|
+
models.DatasetLock.state: LockState.STUCK
|
|
1339
|
+
})
|
|
1340
|
+
session.execute(stmt)
|
|
1341
|
+
logger(logging.DEBUG, '%s while repairing rule %s', type(error).__name__, rule_id)
|
|
1342
|
+
return
|
|
1343
|
+
|
|
1344
|
+
# Reset the counters
|
|
1345
|
+
logger(logging.DEBUG, "Resetting counters for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
|
|
1346
|
+
rule.locks_ok_cnt = 0
|
|
1347
|
+
rule.locks_replicating_cnt = 0
|
|
1348
|
+
rule.locks_stuck_cnt = 0
|
|
1349
|
+
stmt = select(
|
|
1350
|
+
models.ReplicaLock.state,
|
|
1351
|
+
func.count(models.ReplicaLock.state).label('state_counter')
|
|
1352
|
+
).where(
|
|
1353
|
+
models.ReplicaLock.rule_id == rule.id
|
|
1354
|
+
).group_by(
|
|
1355
|
+
models.ReplicaLock.state
|
|
1356
|
+
)
|
|
1357
|
+
rule_counts = session.execute(stmt).all()
|
|
1358
|
+
for count in rule_counts:
|
|
1359
|
+
if count.state == LockState.OK:
|
|
1360
|
+
rule.locks_ok_cnt = count.state_counter
|
|
1361
|
+
elif count.state == LockState.REPLICATING:
|
|
1362
|
+
rule.locks_replicating_cnt = count.state_counter
|
|
1363
|
+
elif count.state == LockState.STUCK:
|
|
1364
|
+
rule.locks_stuck_cnt = count.state_counter
|
|
1365
|
+
logger(logging.DEBUG, "Finished resetting counters for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
|
|
1366
|
+
|
|
1367
|
+
# Get the did
|
|
1368
|
+
stmt = select(
|
|
1369
|
+
models.DataIdentifier
|
|
1370
|
+
).where(
|
|
1371
|
+
and_(models.DataIdentifier.scope == rule.scope,
|
|
1372
|
+
models.DataIdentifier.name == rule.name)
|
|
1373
|
+
)
|
|
1374
|
+
did = session.execute(stmt).scalar_one()
|
|
1375
|
+
|
|
1376
|
+
# Detect if there is something wrong with the dataset and
|
|
1377
|
+
# make the decisison on soft or hard repair.
|
|
1378
|
+
hard_repair = False
|
|
1379
|
+
if did.did_type != DIDType.FILE:
|
|
1380
|
+
nr_files = rucio.core.did.get_did(scope=rule.scope, name=rule.name, dynamic_depth=DIDType.FILE, session=session)['length']
|
|
1381
|
+
else:
|
|
1382
|
+
nr_files = 1
|
|
1383
|
+
if nr_files * rule.copies != (rule.locks_ok_cnt + rule.locks_stuck_cnt + rule.locks_replicating_cnt):
|
|
1384
|
+
hard_repair = True
|
|
1385
|
+
logger(logging.DEBUG, 'Repairing rule %s in HARD mode', str(rule.id))
|
|
1386
|
+
elif rule.copies > 1 and rule.grouping == RuleGrouping.NONE:
|
|
1387
|
+
hard_repair = True
|
|
1388
|
+
logger(logging.DEBUG, 'Repairing rule %s in HARD mode', str(rule.id))
|
|
1389
|
+
|
|
1390
|
+
# Resolve the did to its contents
|
|
1391
|
+
datasetfiles, locks, replicas, source_replicas = __resolve_did_to_locks_and_replicas(did=did,
|
|
1392
|
+
nowait=True,
|
|
1393
|
+
restrict_rses=[rse['id'] for rse in rses],
|
|
1394
|
+
source_rses=[rse['id'] for rse in source_rses],
|
|
1395
|
+
only_stuck=not hard_repair,
|
|
1396
|
+
session=session)
|
|
1397
|
+
|
|
1398
|
+
session.flush()
|
|
1399
|
+
|
|
1400
|
+
# 1. Try to find missing locks and create them based on grouping
|
|
1401
|
+
if did.did_type != DIDType.FILE and hard_repair:
|
|
1402
|
+
try:
|
|
1403
|
+
__find_missing_locks_and_create_them(datasetfiles=datasetfiles,
|
|
1404
|
+
locks=locks,
|
|
1405
|
+
replicas=replicas,
|
|
1406
|
+
source_replicas=source_replicas,
|
|
1407
|
+
rseselector=rseselector,
|
|
1408
|
+
rule=rule,
|
|
1409
|
+
source_rses=[rse['id'] for rse in source_rses],
|
|
1410
|
+
session=session)
|
|
1411
|
+
except (InsufficientAccountLimit, InsufficientTargetRSEs) as error:
|
|
1412
|
+
rule.state = RuleState.STUCK
|
|
1413
|
+
rule.error = (str(error)[:245] + '...') if len(str(error)) > 245 else str(error)
|
|
1414
|
+
rule.save(session=session)
|
|
1415
|
+
# Insert rule history
|
|
1416
|
+
insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
|
|
1417
|
+
# Try to update the DatasetLocks
|
|
1418
|
+
if rule.grouping != RuleGrouping.NONE:
|
|
1419
|
+
stmt = update(
|
|
1420
|
+
models.DatasetLock
|
|
1421
|
+
).where(
|
|
1422
|
+
models.DatasetLock.rule_id == rule.id
|
|
1423
|
+
).values({
|
|
1424
|
+
models.DatasetLock.state: LockState.STUCK
|
|
1425
|
+
})
|
|
1426
|
+
session.execute(stmt)
|
|
1427
|
+
logger(logging.DEBUG, '%s while repairing rule %s', type(error).__name__, rule_id)
|
|
1428
|
+
return
|
|
1429
|
+
|
|
1430
|
+
session.flush()
|
|
1431
|
+
|
|
1432
|
+
# 2. Try to find surplus locks and remove them
|
|
1433
|
+
if hard_repair:
|
|
1434
|
+
__find_surplus_locks_and_remove_them(datasetfiles=datasetfiles,
|
|
1435
|
+
locks=locks,
|
|
1436
|
+
rule=rule,
|
|
1437
|
+
session=session)
|
|
1438
|
+
|
|
1439
|
+
session.flush()
|
|
1440
|
+
|
|
1441
|
+
# 3. Try to find STUCK locks and repair them based on grouping
|
|
1442
|
+
try:
|
|
1443
|
+
__find_stuck_locks_and_repair_them(datasetfiles=datasetfiles,
|
|
1444
|
+
locks=locks,
|
|
1445
|
+
replicas=replicas,
|
|
1446
|
+
source_replicas=source_replicas,
|
|
1447
|
+
rseselector=rseselector,
|
|
1448
|
+
rule=rule,
|
|
1449
|
+
source_rses=[rse['id'] for rse in source_rses],
|
|
1450
|
+
session=session)
|
|
1451
|
+
except (InsufficientAccountLimit, InsufficientTargetRSEs) as error:
|
|
1452
|
+
rule.state = RuleState.STUCK
|
|
1453
|
+
rule.error = (str(error)[:245] + '...') if len(str(error)) > 245 else str(error)
|
|
1454
|
+
rule.save(session=session)
|
|
1455
|
+
# Insert rule history
|
|
1456
|
+
insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
|
|
1457
|
+
# Try to update the DatasetLocks
|
|
1458
|
+
if rule.grouping != RuleGrouping.NONE:
|
|
1459
|
+
stmt = update(
|
|
1460
|
+
models.DatasetLock
|
|
1461
|
+
).where(
|
|
1462
|
+
models.DatasetLock.rule_id == rule.id
|
|
1463
|
+
).values({
|
|
1464
|
+
models.DatasetLock.state: LockState.STUCK
|
|
1465
|
+
})
|
|
1466
|
+
session.execute(stmt)
|
|
1467
|
+
logger(logging.DEBUG, '%s while repairing rule %s', type(error).__name__, rule_id)
|
|
1468
|
+
return
|
|
1469
|
+
|
|
1470
|
+
# Delete Datasetlocks which are not relevant anymore
|
|
1471
|
+
stmt = select(
|
|
1472
|
+
models.ReplicaLock.rse_id
|
|
1473
|
+
).distinct(
|
|
1474
|
+
).where(
|
|
1475
|
+
models.ReplicaLock.rule_id == rule.id
|
|
1476
|
+
)
|
|
1477
|
+
validated_datasetlock_rse_ids = session.execute(stmt).scalars().all()
|
|
1478
|
+
|
|
1479
|
+
stmt = select(
|
|
1480
|
+
models.DatasetLock
|
|
1481
|
+
).where(
|
|
1482
|
+
models.DatasetLock.rule_id == rule.id
|
|
1483
|
+
)
|
|
1484
|
+
dataset_locks = session.execute(stmt).scalars().all()
|
|
1485
|
+
for dataset_lock in dataset_locks:
|
|
1486
|
+
if dataset_lock.rse_id not in validated_datasetlock_rse_ids:
|
|
1487
|
+
dataset_lock.delete(session=session)
|
|
1488
|
+
|
|
1489
|
+
if rule.locks_stuck_cnt != 0:
|
|
1490
|
+
logger(logging.INFO, 'Rule %s [%d/%d/%d] state=STUCK', str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
|
|
1491
|
+
rule.state = RuleState.STUCK
|
|
1492
|
+
# Insert rule history
|
|
1493
|
+
insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
|
|
1494
|
+
# Try to update the DatasetLocks
|
|
1495
|
+
if rule.grouping != RuleGrouping.NONE:
|
|
1496
|
+
stmt = update(
|
|
1497
|
+
models.DatasetLock
|
|
1498
|
+
).where(
|
|
1499
|
+
models.DatasetLock.rule_id == rule.id
|
|
1500
|
+
).values({
|
|
1501
|
+
models.DatasetLock.state: LockState.STUCK
|
|
1502
|
+
})
|
|
1503
|
+
session.execute(stmt)
|
|
1504
|
+
# TODO: Increase some kind of Stuck Counter here, The rule should at some point be SUSPENDED
|
|
1505
|
+
return
|
|
1506
|
+
|
|
1507
|
+
rule.stuck_at = None
|
|
1508
|
+
|
|
1509
|
+
if rule.locks_replicating_cnt > 0:
|
|
1510
|
+
logger(logging.INFO, 'Rule %s [%d/%d/%d] state=REPLICATING', str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
|
|
1511
|
+
rule.state = RuleState.REPLICATING
|
|
1512
|
+
rule.error = None
|
|
1513
|
+
# Insert rule history
|
|
1514
|
+
insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
|
|
1515
|
+
# Try to update the DatasetLocks
|
|
1516
|
+
if rule.grouping != RuleGrouping.NONE:
|
|
1517
|
+
stmt = update(
|
|
1518
|
+
models.DatasetLock
|
|
1519
|
+
).where(
|
|
1520
|
+
models.DatasetLock.rule_id == rule.id
|
|
1521
|
+
).values({
|
|
1522
|
+
models.DatasetLock.state: LockState.REPLICATING
|
|
1523
|
+
})
|
|
1524
|
+
session.execute(stmt)
|
|
1525
|
+
return
|
|
1526
|
+
|
|
1527
|
+
rule.state = RuleState.OK
|
|
1528
|
+
rule.error = None
|
|
1529
|
+
# Insert rule history
|
|
1530
|
+
insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
|
|
1531
|
+
logger(logging.INFO, 'Rule %s [%d/%d/%d] state=OK', str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
|
|
1532
|
+
|
|
1533
|
+
if rule.grouping != RuleGrouping.NONE:
|
|
1534
|
+
stmt = update(
|
|
1535
|
+
models.DatasetLock
|
|
1536
|
+
).where(
|
|
1537
|
+
models.DatasetLock.rule_id == rule.id
|
|
1538
|
+
).values({
|
|
1539
|
+
models.DatasetLock.state: LockState.OK
|
|
1540
|
+
})
|
|
1541
|
+
session.execute(stmt)
|
|
1542
|
+
session.flush()
|
|
1543
|
+
if rule.notification == RuleNotification.YES:
|
|
1544
|
+
generate_email_for_rule_ok_notification(rule=rule, session=session)
|
|
1545
|
+
generate_rule_notifications(rule=rule, replicating_locks_before=0, session=session)
|
|
1546
|
+
# Try to release potential parent rules
|
|
1547
|
+
rucio.core.rule.release_parent_rule(child_rule_id=rule.id, session=session)
|
|
1548
|
+
|
|
1549
|
+
return
|
|
1550
|
+
|
|
1551
|
+
except NoResultFound:
|
|
1552
|
+
# The rule has been deleted in the meanwhile
|
|
1553
|
+
return
|
|
1554
|
+
|
|
1555
|
+
|
|
1556
|
+
@read_session
|
|
1557
|
+
def get_rule(
|
|
1558
|
+
rule_id: str,
|
|
1559
|
+
*,
|
|
1560
|
+
session: "Session"
|
|
1561
|
+
) -> dict[str, Any]:
|
|
1562
|
+
"""
|
|
1563
|
+
Get a specific replication rule.
|
|
1564
|
+
|
|
1565
|
+
:param rule_id: The rule_id to select.
|
|
1566
|
+
:param session: The database session in use.
|
|
1567
|
+
:raises: RuleNotFound if no Rule can be found.
|
|
1568
|
+
"""
|
|
1569
|
+
|
|
1570
|
+
try:
|
|
1571
|
+
stmt = select(
|
|
1572
|
+
models.ReplicationRule
|
|
1573
|
+
).where(
|
|
1574
|
+
models.ReplicationRule.id == rule_id
|
|
1575
|
+
)
|
|
1576
|
+
rule = session.execute(stmt).scalar_one()
|
|
1577
|
+
return rule.to_dict()
|
|
1578
|
+
except NoResultFound as exc:
|
|
1579
|
+
raise RuleNotFound('No rule with the id %s found' % (rule_id)) from exc
|
|
1580
|
+
except StatementError as exc:
|
|
1581
|
+
raise RucioException('Badly formatted rule id (%s)' % (rule_id)) from exc
|
|
1582
|
+
|
|
1583
|
+
|
|
1584
|
+
@transactional_session
|
|
1585
|
+
def update_rule(
|
|
1586
|
+
rule_id: str,
|
|
1587
|
+
options: dict[str, Any],
|
|
1588
|
+
*,
|
|
1589
|
+
session: "Session"
|
|
1590
|
+
) -> None:
|
|
1591
|
+
"""
|
|
1592
|
+
Update a rules options.
|
|
1593
|
+
|
|
1594
|
+
:param rule_id: The rule_id to lock.
|
|
1595
|
+
:param options: Dictionary of options
|
|
1596
|
+
:param session: The database session in use.
|
|
1597
|
+
:raises: RuleNotFound if no Rule can be found, InputValidationError if invalid option is used, ScratchDiskLifetimeConflict if wrong ScratchDiskLifetime is used.
|
|
1598
|
+
"""
|
|
1599
|
+
|
|
1600
|
+
valid_options = ['comment', 'locked', 'lifetime', 'account', 'state',
|
|
1601
|
+
'activity', 'source_replica_expression', 'cancel_requests',
|
|
1602
|
+
'priority', 'child_rule_id', 'eol_at', 'meta',
|
|
1603
|
+
'purge_replicas', 'boost_rule']
|
|
1604
|
+
|
|
1605
|
+
for key in options:
|
|
1606
|
+
if key not in valid_options:
|
|
1607
|
+
raise InputValidationError('%s is not a valid option to set.' % key)
|
|
1608
|
+
|
|
1609
|
+
try:
|
|
1610
|
+
query = select(
|
|
1611
|
+
models.ReplicationRule
|
|
1612
|
+
).where(
|
|
1613
|
+
models.ReplicationRule.id == rule_id
|
|
1614
|
+
)
|
|
1615
|
+
rule: models.ReplicationRule = session.execute(query).scalar_one()
|
|
1616
|
+
|
|
1617
|
+
for key in options:
|
|
1618
|
+
if key == 'lifetime':
|
|
1619
|
+
# Check SCRATCHDISK Policy
|
|
1620
|
+
vo = rule.account.vo
|
|
1621
|
+
rses = parse_expression(rule.rse_expression, filter_={'vo': vo}, session=session)
|
|
1622
|
+
try:
|
|
1623
|
+
lifetime = get_scratch_policy(rule.account, rses, options['lifetime'], session=session)
|
|
1624
|
+
except UndefinedPolicy:
|
|
1625
|
+
lifetime = options['lifetime']
|
|
1626
|
+
rule.expires_at = datetime.utcnow() + timedelta(seconds=lifetime) if lifetime is not None else None
|
|
1627
|
+
if key == 'source_replica_expression':
|
|
1628
|
+
rule.source_replica_expression = options['source_replica_expression']
|
|
1629
|
+
|
|
1630
|
+
if key == 'comment':
|
|
1631
|
+
rule.comments = options['comment']
|
|
1632
|
+
|
|
1633
|
+
if key == 'activity':
|
|
1634
|
+
validate_schema(
|
|
1635
|
+
'activity', options['activity'], vo=rule.account.vo
|
|
1636
|
+
)
|
|
1637
|
+
rule.activity = options['activity']
|
|
1638
|
+
# Cancel transfers and re-submit them:
|
|
1639
|
+
query = select(
|
|
1640
|
+
models.ReplicaLock
|
|
1641
|
+
).where(
|
|
1642
|
+
models.ReplicaLock.rule_id == rule.id,
|
|
1643
|
+
models.ReplicaLock.state == LockState.REPLICATING
|
|
1644
|
+
)
|
|
1645
|
+
for lock in session.execute(query).scalars().all():
|
|
1646
|
+
transfers_to_cancel = request_core.cancel_request_did(
|
|
1647
|
+
scope=lock.scope,
|
|
1648
|
+
name=lock.name,
|
|
1649
|
+
dest_rse_id=lock.rse_id,
|
|
1650
|
+
session=session
|
|
1651
|
+
)
|
|
1652
|
+
transfer_core.cancel_transfers(transfers_to_cancel)
|
|
1653
|
+
query = select(
|
|
1654
|
+
models.RSEFileAssociation.md5,
|
|
1655
|
+
models.RSEFileAssociation.bytes,
|
|
1656
|
+
models.RSEFileAssociation.adler32
|
|
1657
|
+
).where(
|
|
1658
|
+
models.RSEFileAssociation.scope == lock.scope,
|
|
1659
|
+
models.RSEFileAssociation.name == lock.name,
|
|
1660
|
+
models.RSEFileAssociation.rse_id == lock.rse_id
|
|
1661
|
+
)
|
|
1662
|
+
md5, bytes_, adler32 = session.execute(query).one()
|
|
1663
|
+
session.flush()
|
|
1664
|
+
|
|
1665
|
+
requests = create_transfer_dict(
|
|
1666
|
+
dest_rse_id=lock.rse_id,
|
|
1667
|
+
request_type=RequestType.TRANSFER,
|
|
1668
|
+
scope=lock.scope,
|
|
1669
|
+
name=lock.name,
|
|
1670
|
+
rule=rule,
|
|
1671
|
+
lock=lock,
|
|
1672
|
+
bytes_=bytes_,
|
|
1673
|
+
md5=md5,
|
|
1674
|
+
adler32=adler32,
|
|
1675
|
+
ds_scope=rule.scope,
|
|
1676
|
+
ds_name=rule.name,
|
|
1677
|
+
copy_pin_lifetime=None,
|
|
1678
|
+
activity=rule.activity,
|
|
1679
|
+
session=session
|
|
1680
|
+
)
|
|
1681
|
+
request_core.queue_requests([requests], session=session)
|
|
1682
|
+
|
|
1683
|
+
elif key == 'account':
|
|
1684
|
+
# Check if the account exists
|
|
1685
|
+
get_account(options['account'], session=session)
|
|
1686
|
+
# Update locks
|
|
1687
|
+
query = select(
|
|
1688
|
+
models.ReplicaLock
|
|
1689
|
+
).where(
|
|
1690
|
+
models.ReplicaLock.rule_id == rule.id
|
|
1691
|
+
)
|
|
1692
|
+
counter_rses = {}
|
|
1693
|
+
for lock in session.execute(query).scalars().all():
|
|
1694
|
+
if lock.rse_id in counter_rses:
|
|
1695
|
+
counter_rses[lock.rse_id].append(lock.bytes)
|
|
1696
|
+
else:
|
|
1697
|
+
counter_rses[lock.rse_id] = [lock.bytes]
|
|
1698
|
+
for locktype in (models.ReplicaLock, models.DatasetLock):
|
|
1699
|
+
query = update(
|
|
1700
|
+
locktype
|
|
1701
|
+
).where(
|
|
1702
|
+
locktype.rule_id == rule.id
|
|
1703
|
+
).values({
|
|
1704
|
+
locktype.account: options['account']
|
|
1705
|
+
})
|
|
1706
|
+
session.execute(query)
|
|
1707
|
+
|
|
1708
|
+
# Update counters
|
|
1709
|
+
for rse_id in counter_rses:
|
|
1710
|
+
account_counter.decrease(
|
|
1711
|
+
rse_id=rse_id,
|
|
1712
|
+
account=rule.account,
|
|
1713
|
+
files=len(counter_rses[rse_id]),
|
|
1714
|
+
bytes_=sum(counter_rses[rse_id]),
|
|
1715
|
+
session=session
|
|
1716
|
+
)
|
|
1717
|
+
account_counter.increase(
|
|
1718
|
+
rse_id=rse_id,
|
|
1719
|
+
account=options['account'],
|
|
1720
|
+
files=len(counter_rses[rse_id]),
|
|
1721
|
+
bytes_=sum(counter_rses[rse_id]),
|
|
1722
|
+
session=session
|
|
1723
|
+
)
|
|
1724
|
+
# Update rule
|
|
1725
|
+
rule.account = options['account']
|
|
1726
|
+
session.flush()
|
|
1727
|
+
|
|
1728
|
+
elif key == 'state':
|
|
1729
|
+
if options.get('cancel_requests', False):
|
|
1730
|
+
rule_ids_to_stuck = set()
|
|
1731
|
+
query = select(
|
|
1732
|
+
models.ReplicaLock
|
|
1733
|
+
).where(
|
|
1734
|
+
models.ReplicaLock.rule_id == rule.id,
|
|
1735
|
+
models.ReplicaLock.state == LockState.REPLICATING
|
|
1736
|
+
)
|
|
1737
|
+
for lock in session.execute(query).scalars().all():
|
|
1738
|
+
# Set locks to stuck:
|
|
1739
|
+
query = select(
|
|
1740
|
+
models.ReplicaLock
|
|
1741
|
+
).where(
|
|
1742
|
+
models.ReplicaLock.scope == lock.scope,
|
|
1743
|
+
models.ReplicaLock.name == lock.name,
|
|
1744
|
+
models.ReplicaLock.rse_id == lock.rse_id,
|
|
1745
|
+
models.ReplicaLock.state == LockState.REPLICATING
|
|
1746
|
+
)
|
|
1747
|
+
for lock2 in session.execute(query).scalars().all():
|
|
1748
|
+
lock2.state = LockState.STUCK
|
|
1749
|
+
rule_ids_to_stuck.add(lock2.rule_id)
|
|
1750
|
+
transfers_to_cancel = request_core.cancel_request_did(
|
|
1751
|
+
scope=lock.scope,
|
|
1752
|
+
name=lock.name,
|
|
1753
|
+
dest_rse_id=lock.rse_id,
|
|
1754
|
+
session=session
|
|
1755
|
+
)
|
|
1756
|
+
transfer_core.cancel_transfers(transfers_to_cancel)
|
|
1757
|
+
query = select(
|
|
1758
|
+
models.RSEFileAssociation
|
|
1759
|
+
).where(
|
|
1760
|
+
models.RSEFileAssociation.scope == lock.scope,
|
|
1761
|
+
models.RSEFileAssociation.name == lock.name,
|
|
1762
|
+
models.RSEFileAssociation.rse_id == lock.rse_id
|
|
1763
|
+
)
|
|
1764
|
+
replica = session.execute(query).scalar_one()
|
|
1765
|
+
replica.state = ReplicaState.UNAVAILABLE
|
|
1766
|
+
# Set rules and DATASETLOCKS to STUCK:
|
|
1767
|
+
for rid in rule_ids_to_stuck:
|
|
1768
|
+
query = update(
|
|
1769
|
+
models.ReplicationRule
|
|
1770
|
+
).where(
|
|
1771
|
+
models.ReplicationRule.id == rid,
|
|
1772
|
+
models.ReplicationRule.state != RuleState.SUSPENDED
|
|
1773
|
+
).values({
|
|
1774
|
+
models.ReplicationRule.state: RuleState.STUCK
|
|
1775
|
+
})
|
|
1776
|
+
session.execute(query)
|
|
1777
|
+
|
|
1778
|
+
query = update(
|
|
1779
|
+
models.DatasetLock
|
|
1780
|
+
).where(
|
|
1781
|
+
models.DatasetLock.rule_id == rid
|
|
1782
|
+
).values({
|
|
1783
|
+
models.DatasetLock.state: LockState.STUCK
|
|
1784
|
+
})
|
|
1785
|
+
session.execute(query)
|
|
1786
|
+
|
|
1787
|
+
if options['state'].lower() == 'suspended':
|
|
1788
|
+
rule.state = RuleState.SUSPENDED
|
|
1789
|
+
|
|
1790
|
+
elif options['state'].lower() == 'stuck':
|
|
1791
|
+
rule.state = RuleState.STUCK
|
|
1792
|
+
rule.stuck_at = datetime.utcnow()
|
|
1793
|
+
if not options.get('cancel_requests', False):
|
|
1794
|
+
query = update(
|
|
1795
|
+
models.ReplicaLock
|
|
1796
|
+
).where(
|
|
1797
|
+
models.ReplicaLock.rule_id == rule.id,
|
|
1798
|
+
models.ReplicaLock.state == LockState.REPLICATING
|
|
1799
|
+
).values({
|
|
1800
|
+
models.ReplicaLock.state: LockState.STUCK
|
|
1801
|
+
})
|
|
1802
|
+
session.execute(query)
|
|
1803
|
+
|
|
1804
|
+
query = update(
|
|
1805
|
+
models.DatasetLock
|
|
1806
|
+
).where(
|
|
1807
|
+
models.DatasetLock.rule_id == rule_id
|
|
1808
|
+
).values({
|
|
1809
|
+
models.DatasetLock.state: LockState.STUCK
|
|
1810
|
+
})
|
|
1811
|
+
session.execute(query)
|
|
1812
|
+
|
|
1813
|
+
elif key == 'cancel_requests':
|
|
1814
|
+
pass
|
|
1815
|
+
|
|
1816
|
+
elif key == 'priority':
|
|
1817
|
+
try:
|
|
1818
|
+
rule.priority = options[key]
|
|
1819
|
+
transfers_to_update = request_core.update_requests_priority(priority=options[key], filter_={'rule_id': rule_id}, session=session)
|
|
1820
|
+
transfer_core.update_transfer_priority(transfers_to_update)
|
|
1821
|
+
except Exception as exc:
|
|
1822
|
+
raise UnsupportedOperation('The FTS Requests are already in a final state.') from exc
|
|
1823
|
+
|
|
1824
|
+
elif key == 'child_rule_id':
|
|
1825
|
+
# Check if the child rule has the same scope/name as the parent rule
|
|
1826
|
+
child_id: Optional[str] = options[key]
|
|
1827
|
+
if child_id is None:
|
|
1828
|
+
if not rule.child_rule_id:
|
|
1829
|
+
raise InputValidationError('Cannot detach child when no such relationship exists')
|
|
1830
|
+
# dissolve relationship
|
|
1831
|
+
rule.child_rule_id = None # type: ignore
|
|
1832
|
+
# remove expiration date
|
|
1833
|
+
rule.expires_at = None # type: ignore
|
|
1834
|
+
else:
|
|
1835
|
+
query = select(
|
|
1836
|
+
models.ReplicationRule
|
|
1837
|
+
).where(
|
|
1838
|
+
models.ReplicationRule.id == child_id
|
|
1839
|
+
)
|
|
1840
|
+
child_rule = session.execute(query).scalar_one()
|
|
1841
|
+
if rule.scope != child_rule.scope or rule.name != child_rule.name:
|
|
1842
|
+
raise InputValidationError('Parent and child rule must be set on the same dataset.')
|
|
1843
|
+
if rule.id == options[key]:
|
|
1844
|
+
raise InputValidationError('Self-referencing parent/child-relationship.')
|
|
1845
|
+
if child_rule.state != RuleState.OK:
|
|
1846
|
+
rule.child_rule_id = child_id # type: ignore
|
|
1847
|
+
|
|
1848
|
+
elif key == 'meta':
|
|
1849
|
+
# Need to json.dump the metadata
|
|
1850
|
+
rule.meta = json.dumps(options[key])
|
|
1851
|
+
|
|
1852
|
+
else:
|
|
1853
|
+
setattr(rule, key, options[key])
|
|
1854
|
+
|
|
1855
|
+
insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
|
|
1856
|
+
|
|
1857
|
+
# `boost_rule` should run after `stuck`, so lets not include it in the loop since the arguments are unordered
|
|
1858
|
+
if 'boost_rule' in options:
|
|
1859
|
+
query = select(
|
|
1860
|
+
models.ReplicaLock
|
|
1861
|
+
).where(
|
|
1862
|
+
models.ReplicaLock.rule_id == rule.id,
|
|
1863
|
+
models.ReplicaLock.state == LockState.STUCK
|
|
1864
|
+
)
|
|
1865
|
+
for lock in session.execute(query).scalars().all():
|
|
1866
|
+
lock['updated_at'] -= timedelta(days=1)
|
|
1867
|
+
|
|
1868
|
+
rule['updated_at'] -= timedelta(days=1)
|
|
1869
|
+
|
|
1870
|
+
insert_rule_history(
|
|
1871
|
+
rule,
|
|
1872
|
+
recent=True,
|
|
1873
|
+
longterm=False,
|
|
1874
|
+
session=session
|
|
1875
|
+
)
|
|
1876
|
+
|
|
1877
|
+
except IntegrityError as error:
|
|
1878
|
+
if match('.*ORA-00001.*', str(error.args[0])) \
|
|
1879
|
+
or match('.*IntegrityError.*UNIQUE constraint failed.*', str(error.args[0])) \
|
|
1880
|
+
or match('.*1062.*Duplicate entry.*for key.*', str(error.args[0])) \
|
|
1881
|
+
or match('.*IntegrityError.*columns? .*not unique.*', str(error.args[0])):
|
|
1882
|
+
raise DuplicateRule(error.args[0]) from error
|
|
1883
|
+
else:
|
|
1884
|
+
raise error
|
|
1885
|
+
except NoResultFound as exc:
|
|
1886
|
+
raise RuleNotFound('No rule with the id %s found' % (rule_id)) from exc
|
|
1887
|
+
except StatementError as exc:
|
|
1888
|
+
raise RucioException(f"A StatementError occurred while processing rule {rule_id}") from exc
|
|
1889
|
+
|
|
1890
|
+
|
|
1891
|
+
@transactional_session
|
|
1892
|
+
def reduce_rule(
|
|
1893
|
+
rule_id: str,
|
|
1894
|
+
copies: int,
|
|
1895
|
+
exclude_expression: Optional[str] = None,
|
|
1896
|
+
*,
|
|
1897
|
+
session: "Session"
|
|
1898
|
+
) -> str:
|
|
1899
|
+
"""
|
|
1900
|
+
Reduce the number of copies for a rule by atomically replacing the rule.
|
|
1901
|
+
|
|
1902
|
+
:param rule_id: Rule to be reduced.
|
|
1903
|
+
:param copies: Number of copies of the new rule.
|
|
1904
|
+
:param exclude_expression: RSE Expression of RSEs to exclude.
|
|
1905
|
+
:param session: The DB Session.
|
|
1906
|
+
:raises: RuleReplaceFailed, RuleNotFound
|
|
1907
|
+
"""
|
|
1908
|
+
try:
|
|
1909
|
+
stmt = select(
|
|
1910
|
+
models.ReplicationRule
|
|
1911
|
+
).where(
|
|
1912
|
+
models.ReplicationRule.id == rule_id
|
|
1913
|
+
)
|
|
1914
|
+
rule = session.execute(stmt).scalar_one()
|
|
1915
|
+
|
|
1916
|
+
if copies >= rule.copies:
|
|
1917
|
+
raise RuleReplaceFailed('Copies of the new rule must be smaller than the old rule.')
|
|
1918
|
+
|
|
1919
|
+
if rule.state != RuleState.OK:
|
|
1920
|
+
raise RuleReplaceFailed('The source rule must be in state OK.')
|
|
1921
|
+
|
|
1922
|
+
if exclude_expression:
|
|
1923
|
+
rse_expression = '(' + rule.rse_expression + ')' + '\\' + '(' + exclude_expression + ')'
|
|
1924
|
+
else:
|
|
1925
|
+
rse_expression = rule.rse_expression
|
|
1926
|
+
|
|
1927
|
+
grouping = {RuleGrouping.ALL: 'ALL', RuleGrouping.NONE: 'NONE'}.get(rule.grouping, 'DATASET')
|
|
1928
|
+
|
|
1929
|
+
if rule.expires_at:
|
|
1930
|
+
lifetime = (rule.expires_at - datetime.utcnow()).days * 24 * 3600 + (rule.expires_at - datetime.utcnow()).seconds
|
|
1931
|
+
else:
|
|
1932
|
+
lifetime = None
|
|
1933
|
+
|
|
1934
|
+
notify = {RuleNotification.YES: 'Y', RuleNotification.CLOSE: 'C', RuleNotification.PROGRESS: 'P'}.get(rule.notification, 'N')
|
|
1935
|
+
|
|
1936
|
+
new_rule_id = add_rule(dids=[{'scope': rule.scope, 'name': rule.name}],
|
|
1937
|
+
account=rule.account,
|
|
1938
|
+
copies=copies,
|
|
1939
|
+
rse_expression=rse_expression,
|
|
1940
|
+
grouping=grouping,
|
|
1941
|
+
weight=rule.weight,
|
|
1942
|
+
lifetime=lifetime,
|
|
1943
|
+
locked=rule.locked,
|
|
1944
|
+
subscription_id=rule.subscription_id,
|
|
1945
|
+
source_replica_expression=rule.source_replica_expression,
|
|
1946
|
+
activity=rule.activity,
|
|
1947
|
+
notify=notify,
|
|
1948
|
+
purge_replicas=rule.purge_replicas,
|
|
1949
|
+
ignore_availability=rule.ignore_availability,
|
|
1950
|
+
session=session)
|
|
1951
|
+
|
|
1952
|
+
session.flush()
|
|
1953
|
+
|
|
1954
|
+
stmt = select(
|
|
1955
|
+
models.ReplicationRule
|
|
1956
|
+
).where(
|
|
1957
|
+
models.ReplicationRule.id == new_rule_id[0]
|
|
1958
|
+
)
|
|
1959
|
+
new_rule = session.execute(stmt).scalar_one()
|
|
1960
|
+
|
|
1961
|
+
if new_rule.state != RuleState.OK:
|
|
1962
|
+
raise RuleReplaceFailed('The replacement of the rule failed.')
|
|
1963
|
+
|
|
1964
|
+
delete_rule(rule_id=rule_id,
|
|
1965
|
+
session=session)
|
|
1966
|
+
|
|
1967
|
+
return new_rule_id[0]
|
|
1968
|
+
|
|
1969
|
+
except NoResultFound as exc:
|
|
1970
|
+
raise RuleNotFound('No rule with the id %s found' % (rule_id)) from exc
|
|
1971
|
+
|
|
1972
|
+
|
|
1973
|
+
@transactional_session
|
|
1974
|
+
def move_rule(
|
|
1975
|
+
rule_id: str,
|
|
1976
|
+
rse_expression: str,
|
|
1977
|
+
override: Optional[dict[str, Any]] = None,
|
|
1978
|
+
*,
|
|
1979
|
+
session: "Session"
|
|
1980
|
+
) -> str:
|
|
1981
|
+
"""
|
|
1982
|
+
Move a replication rule to another RSE and, once done, delete the original one.
|
|
1983
|
+
|
|
1984
|
+
:param rule_id: Rule to be moved.
|
|
1985
|
+
:param rse_expression: RSE expression of the new rule.
|
|
1986
|
+
:param override: Configurations to update for the new rule.
|
|
1987
|
+
:param session: The DB Session.
|
|
1988
|
+
:raises: RuleNotFound, RuleReplaceFailed, InvalidRSEExpression
|
|
1989
|
+
"""
|
|
1990
|
+
override = override or {}
|
|
1991
|
+
|
|
1992
|
+
try:
|
|
1993
|
+
stmt = select(
|
|
1994
|
+
models.ReplicationRule
|
|
1995
|
+
).where(
|
|
1996
|
+
models.ReplicationRule.id == rule_id
|
|
1997
|
+
)
|
|
1998
|
+
rule = session.execute(stmt).scalar_one()
|
|
1999
|
+
|
|
2000
|
+
if rule.child_rule_id:
|
|
2001
|
+
raise RuleReplaceFailed('The rule must not have a child rule.')
|
|
2002
|
+
|
|
2003
|
+
grouping = {RuleGrouping.ALL: 'ALL', RuleGrouping.NONE: 'NONE'}.get(rule.grouping, 'DATASET')
|
|
2004
|
+
|
|
2005
|
+
if rule.expires_at:
|
|
2006
|
+
lifetime = (rule.expires_at - datetime.utcnow()).days * 24 * 3600 + (rule.expires_at - datetime.utcnow()).seconds
|
|
2007
|
+
else:
|
|
2008
|
+
lifetime = None
|
|
2009
|
+
|
|
2010
|
+
notify = {RuleNotification.YES: 'Y', RuleNotification.CLOSE: 'C', RuleNotification.PROGRESS: 'P'}.get(rule.notification, 'N')
|
|
2011
|
+
|
|
2012
|
+
options = {
|
|
2013
|
+
'dids': [{'scope': rule.scope, 'name': rule.name}],
|
|
2014
|
+
'account': rule.account,
|
|
2015
|
+
'copies': rule.copies,
|
|
2016
|
+
'rse_expression': rse_expression,
|
|
2017
|
+
'grouping': grouping,
|
|
2018
|
+
'weight': rule.weight,
|
|
2019
|
+
'lifetime': lifetime,
|
|
2020
|
+
'locked': rule.locked,
|
|
2021
|
+
'subscription_id': rule.subscription_id,
|
|
2022
|
+
'source_replica_expression': rule.source_replica_expression,
|
|
2023
|
+
'activity': rule.activity,
|
|
2024
|
+
'notify': notify,
|
|
2025
|
+
'purge_replicas': rule.purge_replicas,
|
|
2026
|
+
'ignore_availability': rule.ignore_availability,
|
|
2027
|
+
'comment': rule.comments,
|
|
2028
|
+
'session': session,
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
for key in override:
|
|
2032
|
+
if key in ['dids', 'session']:
|
|
2033
|
+
raise UnsupportedOperation('Not allowed to override option %s' % key)
|
|
2034
|
+
elif key not in options:
|
|
2035
|
+
raise UnsupportedOperation('Non-valid override option %s' % key)
|
|
2036
|
+
else:
|
|
2037
|
+
options[key] = override[key]
|
|
2038
|
+
|
|
2039
|
+
new_rule_id = add_rule(**options)
|
|
2040
|
+
|
|
2041
|
+
session.flush()
|
|
2042
|
+
|
|
2043
|
+
update_rule(rule_id=rule_id, options={'child_rule_id': new_rule_id[0], 'lifetime': 0}, session=session)
|
|
2044
|
+
|
|
2045
|
+
return new_rule_id[0]
|
|
2046
|
+
|
|
2047
|
+
except StatementError as exc:
|
|
2048
|
+
raise RucioException('Badly formatted rule id (%s)' % (rule_id)) from exc
|
|
2049
|
+
except NoResultFound as exc:
|
|
2050
|
+
raise RuleNotFound('No rule with the id %s found' % (rule_id)) from exc
|
|
2051
|
+
|
|
2052
|
+
|
|
2053
|
+
@transactional_session
|
|
2054
|
+
def re_evaluate_did(
|
|
2055
|
+
scope: InternalScope,
|
|
2056
|
+
name: str,
|
|
2057
|
+
rule_evaluation_action: DIDReEvaluation,
|
|
2058
|
+
*,
|
|
2059
|
+
session: "Session"
|
|
2060
|
+
) -> None:
|
|
2061
|
+
"""
|
|
2062
|
+
Re-Evaluates a did.
|
|
2063
|
+
|
|
2064
|
+
:param scope: The scope of the did to be re-evaluated.
|
|
2065
|
+
:param name: The name of the did to be re-evaluated.
|
|
2066
|
+
:param rule_evaluation_action: The Rule evaluation action.
|
|
2067
|
+
:param session: The database session in use.
|
|
2068
|
+
:raises: DataIdentifierNotFound
|
|
2069
|
+
"""
|
|
2070
|
+
|
|
2071
|
+
try:
|
|
2072
|
+
stmt = select(
|
|
2073
|
+
models.DataIdentifier
|
|
2074
|
+
).where(
|
|
2075
|
+
and_(models.DataIdentifier.scope == scope,
|
|
2076
|
+
models.DataIdentifier.name == name)
|
|
2077
|
+
)
|
|
2078
|
+
did = session.execute(stmt).scalar_one()
|
|
2079
|
+
except NoResultFound as exc:
|
|
2080
|
+
raise DataIdentifierNotFound() from exc
|
|
2081
|
+
|
|
2082
|
+
if rule_evaluation_action == DIDReEvaluation.ATTACH:
|
|
2083
|
+
__evaluate_did_attach(did, session=session)
|
|
2084
|
+
else:
|
|
2085
|
+
__evaluate_did_detach(did, session=session)
|
|
2086
|
+
|
|
2087
|
+
# Update size and length of did
|
|
2088
|
+
if session.bind.dialect.name == 'oracle':
|
|
2089
|
+
stmt = select(
|
|
2090
|
+
func.sum(models.DataIdentifierAssociation.bytes),
|
|
2091
|
+
func.count(1)
|
|
2092
|
+
).with_hint(
|
|
2093
|
+
models.DataIdentifierAssociation, 'INDEX(CONTENTS CONTENTS_PK)', 'oracle'
|
|
2094
|
+
).where(
|
|
2095
|
+
and_(models.DataIdentifierAssociation.scope == scope,
|
|
2096
|
+
models.DataIdentifierAssociation.name == name)
|
|
2097
|
+
)
|
|
2098
|
+
for bytes_, length in session.execute(stmt):
|
|
2099
|
+
did.bytes = bytes_
|
|
2100
|
+
did.length = length
|
|
2101
|
+
|
|
2102
|
+
# Add an updated_col_rep
|
|
2103
|
+
if did.did_type == DIDType.DATASET:
|
|
2104
|
+
models.UpdatedCollectionReplica(scope=scope,
|
|
2105
|
+
name=name,
|
|
2106
|
+
did_type=did.did_type).save(session=session)
|
|
2107
|
+
|
|
2108
|
+
|
|
2109
|
+
@read_session
|
|
2110
|
+
def get_updated_dids(
|
|
2111
|
+
total_workers: int,
|
|
2112
|
+
worker_number: int,
|
|
2113
|
+
limit: int = 100,
|
|
2114
|
+
blocked_dids: Optional['Sequence[tuple[str, str]]'] = None,
|
|
2115
|
+
*,
|
|
2116
|
+
session: "Session"
|
|
2117
|
+
) -> list[tuple[str, InternalScope, str, DIDReEvaluation]]:
|
|
2118
|
+
"""
|
|
2119
|
+
Get updated dids.
|
|
2120
|
+
|
|
2121
|
+
:param total_workers: Number of total workers.
|
|
2122
|
+
:param worker_number: id of the executing worker.
|
|
2123
|
+
:param limit: Maximum number of dids to return.
|
|
2124
|
+
:param blocked_dids: Blocked dids to filter.
|
|
2125
|
+
:param session: Database session in use.
|
|
2126
|
+
"""
|
|
2127
|
+
blocked_dids = blocked_dids or []
|
|
2128
|
+
stmt = select(
|
|
2129
|
+
models.UpdatedDID.id,
|
|
2130
|
+
models.UpdatedDID.scope,
|
|
2131
|
+
models.UpdatedDID.name,
|
|
2132
|
+
models.UpdatedDID.rule_evaluation_action
|
|
2133
|
+
)
|
|
2134
|
+
stmt = filter_thread_work(session=session, query=stmt, total_threads=total_workers, thread_id=worker_number, hash_variable='name')
|
|
2135
|
+
|
|
2136
|
+
# Remove blocked dids from query, but only do the first 30 ones, not to overload the query
|
|
2137
|
+
if blocked_dids:
|
|
2138
|
+
chunk = list(chunks(blocked_dids, 30))[0]
|
|
2139
|
+
stmt = stmt.where(tuple_(models.UpdatedDID.scope, models.UpdatedDID.name).notin_(chunk))
|
|
2140
|
+
|
|
2141
|
+
if limit:
|
|
2142
|
+
fetched_dids = session.execute(stmt.order_by(models.UpdatedDID.created_at).limit(limit)).all()
|
|
2143
|
+
filtered_dids = [did._tuple() for did in fetched_dids if (did.scope, did.name) not in blocked_dids]
|
|
2144
|
+
if len(fetched_dids) == limit and not filtered_dids:
|
|
2145
|
+
return get_updated_dids(total_workers=total_workers,
|
|
2146
|
+
worker_number=worker_number,
|
|
2147
|
+
limit=None,
|
|
2148
|
+
blocked_dids=blocked_dids,
|
|
2149
|
+
session=session)
|
|
2150
|
+
else:
|
|
2151
|
+
return filtered_dids
|
|
2152
|
+
else:
|
|
2153
|
+
return [did._tuple() for did in session.execute(stmt.order_by(models.UpdatedDID.created_at)).all() if (did.scope, did.name) not in blocked_dids]
|
|
2154
|
+
|
|
2155
|
+
|
|
2156
|
+
@read_session
|
|
2157
|
+
def get_rules_beyond_eol(
|
|
2158
|
+
date_check: datetime,
|
|
2159
|
+
worker_number: int,
|
|
2160
|
+
total_workers: int, *,
|
|
2161
|
+
session: "Session"
|
|
2162
|
+
) -> list[tuple[InternalScope,
|
|
2163
|
+
str,
|
|
2164
|
+
str,
|
|
2165
|
+
bool,
|
|
2166
|
+
str,
|
|
2167
|
+
Optional[datetime],
|
|
2168
|
+
Optional[datetime],
|
|
2169
|
+
InternalAccount]]:
|
|
2170
|
+
"""
|
|
2171
|
+
Get rules which have eol_at before a certain date.
|
|
2172
|
+
|
|
2173
|
+
:param date_check: The reference date that should be compared to eol_at.
|
|
2174
|
+
:param worker_number: id of the executing worker.
|
|
2175
|
+
:param total_workers: Number of total workers.
|
|
2176
|
+
:param session: Database session in use.
|
|
2177
|
+
"""
|
|
2178
|
+
stmt = select(
|
|
2179
|
+
models.ReplicationRule.scope,
|
|
2180
|
+
models.ReplicationRule.name,
|
|
2181
|
+
models.ReplicationRule.rse_expression,
|
|
2182
|
+
models.ReplicationRule.locked,
|
|
2183
|
+
models.ReplicationRule.id,
|
|
2184
|
+
models.ReplicationRule.eol_at,
|
|
2185
|
+
models.ReplicationRule.expires_at,
|
|
2186
|
+
models.ReplicationRule.account
|
|
2187
|
+
).where(
|
|
2188
|
+
models.ReplicationRule.eol_at < date_check
|
|
2189
|
+
)
|
|
2190
|
+
|
|
2191
|
+
stmt = filter_thread_work(session=session, query=stmt, total_threads=total_workers, thread_id=worker_number, hash_variable='name')
|
|
2192
|
+
return [row._tuple() for row in session.execute(stmt).all()]
|
|
2193
|
+
|
|
2194
|
+
|
|
2195
|
+
@read_session
|
|
2196
|
+
def get_expired_rules(
|
|
2197
|
+
total_workers: int,
|
|
2198
|
+
worker_number: int,
|
|
2199
|
+
limit: int = 100,
|
|
2200
|
+
blocked_rules: Optional['Sequence[str]'] = None,
|
|
2201
|
+
*,
|
|
2202
|
+
session: "Session"
|
|
2203
|
+
) -> list[tuple[str, str]]:
|
|
2204
|
+
"""
|
|
2205
|
+
Get expired rules.
|
|
2206
|
+
|
|
2207
|
+
:param total_workers: Number of total workers.
|
|
2208
|
+
:param worker_number: id of the executing worker.
|
|
2209
|
+
:param limit: Maximum number of rules to return.
|
|
2210
|
+
:param blocked_rules: List of blocked rules.
|
|
2211
|
+
:param session: Database session in use.
|
|
2212
|
+
"""
|
|
2213
|
+
|
|
2214
|
+
blocked_rules = blocked_rules or []
|
|
2215
|
+
stmt = select(
|
|
2216
|
+
models.ReplicationRule.id,
|
|
2217
|
+
models.ReplicationRule.rse_expression
|
|
2218
|
+
).with_hint(
|
|
2219
|
+
models.ReplicationRule, 'INDEX(RULES RULES_EXPIRES_AT_IDX)', 'oracle'
|
|
2220
|
+
).where(
|
|
2221
|
+
and_(models.ReplicationRule.expires_at < datetime.utcnow(),
|
|
2222
|
+
models.ReplicationRule.locked == false(),
|
|
2223
|
+
models.ReplicationRule.child_rule_id == null())
|
|
2224
|
+
).order_by(
|
|
2225
|
+
models.ReplicationRule.expires_at
|
|
2226
|
+
)
|
|
2227
|
+
stmt = filter_thread_work(session=session, query=stmt, total_threads=total_workers, thread_id=worker_number, hash_variable='name')
|
|
2228
|
+
|
|
2229
|
+
if limit:
|
|
2230
|
+
stmt = stmt.limit(limit)
|
|
2231
|
+
result = session.execute(stmt).all()
|
|
2232
|
+
filtered_rules = [rule._tuple() for rule in result if rule.id not in blocked_rules]
|
|
2233
|
+
if len(result) == limit and not filtered_rules:
|
|
2234
|
+
return get_expired_rules(total_workers=total_workers,
|
|
2235
|
+
worker_number=worker_number,
|
|
2236
|
+
limit=None,
|
|
2237
|
+
blocked_rules=blocked_rules,
|
|
2238
|
+
session=session)
|
|
2239
|
+
else:
|
|
2240
|
+
return filtered_rules
|
|
2241
|
+
else:
|
|
2242
|
+
return [rule._tuple() for rule in session.execute(stmt).all() if rule.id not in blocked_rules]
|
|
2243
|
+
|
|
2244
|
+
|
|
2245
|
+
@read_session
|
|
2246
|
+
def get_injected_rules(
|
|
2247
|
+
total_workers: int,
|
|
2248
|
+
worker_number: int,
|
|
2249
|
+
limit: int = 100,
|
|
2250
|
+
blocked_rules: Optional['Sequence[str]'] = None,
|
|
2251
|
+
*,
|
|
2252
|
+
session: "Session"
|
|
2253
|
+
) -> list[str]:
|
|
2254
|
+
"""
|
|
2255
|
+
Get rules to be injected.
|
|
2256
|
+
|
|
2257
|
+
:param total_workers: Number of total workers.
|
|
2258
|
+
:param worker_number: id of the executing worker.
|
|
2259
|
+
:param limit: Maximum number of rules to return.
|
|
2260
|
+
:param blocked_rules: Blocked rules not to include.
|
|
2261
|
+
:param session: Database session in use.
|
|
2262
|
+
"""
|
|
2263
|
+
|
|
2264
|
+
blocked_rules = blocked_rules or []
|
|
2265
|
+
stmt = select(
|
|
2266
|
+
models.ReplicationRule.id
|
|
2267
|
+
).with_hint(
|
|
2268
|
+
models.ReplicationRule, 'INDEX(RULES RULES_STATE_IDX)', 'oracle'
|
|
2269
|
+
).where(
|
|
2270
|
+
and_(models.ReplicationRule.state == RuleState.INJECT,
|
|
2271
|
+
models.ReplicationRule.created_at <= datetime.utcnow())
|
|
2272
|
+
).order_by(
|
|
2273
|
+
models.ReplicationRule.created_at
|
|
2274
|
+
)
|
|
2275
|
+
stmt = filter_thread_work(session=session, query=stmt, total_threads=total_workers, thread_id=worker_number, hash_variable='name')
|
|
2276
|
+
|
|
2277
|
+
if limit:
|
|
2278
|
+
stmt = stmt.limit(limit)
|
|
2279
|
+
result = session.execute(stmt).scalars().all()
|
|
2280
|
+
filtered_rules = [rule for rule in result if rule not in blocked_rules]
|
|
2281
|
+
if len(result) == limit and not filtered_rules:
|
|
2282
|
+
return get_injected_rules(total_workers=total_workers,
|
|
2283
|
+
worker_number=worker_number,
|
|
2284
|
+
limit=None,
|
|
2285
|
+
blocked_rules=blocked_rules,
|
|
2286
|
+
session=session)
|
|
2287
|
+
else:
|
|
2288
|
+
return filtered_rules
|
|
2289
|
+
else:
|
|
2290
|
+
return [rule for rule in session.execute(stmt).scalars().all() if rule not in blocked_rules]
|
|
2291
|
+
|
|
2292
|
+
|
|
2293
|
+
@read_session
|
|
2294
|
+
def get_stuck_rules(
|
|
2295
|
+
total_workers: int,
|
|
2296
|
+
worker_number: int,
|
|
2297
|
+
delta: int = 600,
|
|
2298
|
+
limit: int = 10,
|
|
2299
|
+
blocked_rules: Optional['Sequence[str]'] = None,
|
|
2300
|
+
*,
|
|
2301
|
+
session: "Session"
|
|
2302
|
+
) -> list[str]:
|
|
2303
|
+
"""
|
|
2304
|
+
Get stuck rules.
|
|
2305
|
+
|
|
2306
|
+
:param total_workers: Number of total workers.
|
|
2307
|
+
:param worker_number: id of the executing worker.
|
|
2308
|
+
:param delta: Delta in seconds to select rules in.
|
|
2309
|
+
:param limit: Maximum number of rules to select.
|
|
2310
|
+
:param blocked_rules: Blocked rules to filter out.
|
|
2311
|
+
:param session: Database session in use.
|
|
2312
|
+
"""
|
|
2313
|
+
blocked_rules = blocked_rules or []
|
|
2314
|
+
stmt = select(
|
|
2315
|
+
models.ReplicationRule.id
|
|
2316
|
+
).with_hint(
|
|
2317
|
+
models.ReplicationRule, 'INDEX(RULES RULES_STATE_IDX)', 'oracle'
|
|
2318
|
+
).where(
|
|
2319
|
+
and_(models.ReplicationRule.state == RuleState.STUCK,
|
|
2320
|
+
models.ReplicationRule.updated_at < datetime.utcnow() - timedelta(seconds=delta),
|
|
2321
|
+
or_(models.ReplicationRule.expires_at == null(),
|
|
2322
|
+
models.ReplicationRule.expires_at > datetime.utcnow(),
|
|
2323
|
+
models.ReplicationRule.locked == true()))
|
|
2324
|
+
).order_by(
|
|
2325
|
+
models.ReplicationRule.updated_at
|
|
2326
|
+
)
|
|
2327
|
+
stmt = filter_thread_work(session=session, query=stmt, total_threads=total_workers, thread_id=worker_number, hash_variable='name')
|
|
2328
|
+
|
|
2329
|
+
if limit:
|
|
2330
|
+
stmt = stmt.limit(limit)
|
|
2331
|
+
result = session.execute(stmt).scalars().all()
|
|
2332
|
+
filtered_rules = [rule for rule in result if rule not in blocked_rules]
|
|
2333
|
+
if len(result) == limit and not filtered_rules:
|
|
2334
|
+
return get_stuck_rules(total_workers=total_workers,
|
|
2335
|
+
worker_number=worker_number,
|
|
2336
|
+
delta=delta,
|
|
2337
|
+
limit=None,
|
|
2338
|
+
blocked_rules=blocked_rules,
|
|
2339
|
+
session=session)
|
|
2340
|
+
else:
|
|
2341
|
+
return filtered_rules
|
|
2342
|
+
else:
|
|
2343
|
+
return [rule for rule in session.execute(stmt).scalars().all() if rule not in blocked_rules]
|
|
2344
|
+
|
|
2345
|
+
|
|
2346
|
+
@transactional_session
|
|
2347
|
+
def delete_updated_did(
|
|
2348
|
+
id_: str,
|
|
2349
|
+
*,
|
|
2350
|
+
session: "Session"
|
|
2351
|
+
) -> None:
|
|
2352
|
+
"""
|
|
2353
|
+
Delete an updated_did by id.
|
|
2354
|
+
|
|
2355
|
+
:param id_: Id of the row not to delete.
|
|
2356
|
+
:param session: The database session in use.
|
|
2357
|
+
"""
|
|
2358
|
+
stmt = delete(
|
|
2359
|
+
models.UpdatedDID
|
|
2360
|
+
).where(
|
|
2361
|
+
models.UpdatedDID.id == id_
|
|
2362
|
+
)
|
|
2363
|
+
session.execute(stmt)
|
|
2364
|
+
|
|
2365
|
+
|
|
2366
|
+
@transactional_session
|
|
2367
|
+
def update_rules_for_lost_replica(
|
|
2368
|
+
scope: InternalScope,
|
|
2369
|
+
name: str,
|
|
2370
|
+
rse_id: str,
|
|
2371
|
+
nowait: bool = False,
|
|
2372
|
+
*,
|
|
2373
|
+
session: "Session",
|
|
2374
|
+
logger: LoggerFunction = logging.log
|
|
2375
|
+
) -> None:
|
|
2376
|
+
"""
|
|
2377
|
+
Update rules if a file replica is lost.
|
|
2378
|
+
|
|
2379
|
+
:param scope: Scope of the replica.
|
|
2380
|
+
:param name: Name of the replica.
|
|
2381
|
+
:param rse_id: RSE id of the replica.
|
|
2382
|
+
:param nowait: Nowait parameter for the FOR UPDATE statement.
|
|
2383
|
+
:param session: The database session in use.
|
|
2384
|
+
:param logger: Optional decorated logger that can be passed from the calling daemons or servers.
|
|
2385
|
+
"""
|
|
2386
|
+
|
|
2387
|
+
stmt = select(
|
|
2388
|
+
models.ReplicaLock
|
|
2389
|
+
).where(
|
|
2390
|
+
and_(models.ReplicaLock.scope == scope,
|
|
2391
|
+
models.ReplicaLock.name == name,
|
|
2392
|
+
models.ReplicaLock.rse_id == rse_id)
|
|
2393
|
+
).with_for_update(
|
|
2394
|
+
nowait=nowait
|
|
2395
|
+
)
|
|
2396
|
+
locks = session.execute(stmt).scalars().all()
|
|
2397
|
+
|
|
2398
|
+
stmt = select(
|
|
2399
|
+
models.RSEFileAssociation
|
|
2400
|
+
).where(
|
|
2401
|
+
and_(models.RSEFileAssociation.scope == scope,
|
|
2402
|
+
models.RSEFileAssociation.name == name,
|
|
2403
|
+
models.RSEFileAssociation.rse_id == rse_id)
|
|
2404
|
+
).with_for_update(
|
|
2405
|
+
nowait=nowait
|
|
2406
|
+
)
|
|
2407
|
+
replica = session.execute(stmt).scalar_one()
|
|
2408
|
+
|
|
2409
|
+
stmt = select(
|
|
2410
|
+
models.Request
|
|
2411
|
+
).where(
|
|
2412
|
+
and_(models.Request.scope == scope,
|
|
2413
|
+
models.Request.name == name,
|
|
2414
|
+
models.Request.dest_rse_id == rse_id)
|
|
2415
|
+
).with_for_update(
|
|
2416
|
+
nowait=nowait
|
|
2417
|
+
)
|
|
2418
|
+
requests = session.execute(stmt).scalars().all()
|
|
2419
|
+
|
|
2420
|
+
rse = get_rse_name(rse_id, session=session)
|
|
2421
|
+
|
|
2422
|
+
datasets = []
|
|
2423
|
+
parent_dids = rucio.core.did.list_parent_dids(scope=scope, name=name, session=session)
|
|
2424
|
+
for parent in parent_dids:
|
|
2425
|
+
if {'name': parent['name'], 'scope': parent['scope']} not in datasets:
|
|
2426
|
+
datasets.append({'name': parent['name'], 'scope': parent['scope']})
|
|
2427
|
+
|
|
2428
|
+
for request in requests:
|
|
2429
|
+
session.delete(request)
|
|
2430
|
+
|
|
2431
|
+
for lock in locks:
|
|
2432
|
+
stmt = select(
|
|
2433
|
+
models.ReplicationRule
|
|
2434
|
+
).where(
|
|
2435
|
+
models.ReplicationRule.id == lock.rule_id
|
|
2436
|
+
).with_for_update(
|
|
2437
|
+
nowait=nowait
|
|
2438
|
+
)
|
|
2439
|
+
rule = session.execute(stmt).scalar_one()
|
|
2440
|
+
rule_state_before = rule.state
|
|
2441
|
+
replica.lock_cnt -= 1
|
|
2442
|
+
if lock.state == LockState.OK:
|
|
2443
|
+
rule.locks_ok_cnt -= 1
|
|
2444
|
+
elif lock.state == LockState.REPLICATING:
|
|
2445
|
+
rule.locks_replicating_cnt -= 1
|
|
2446
|
+
elif lock.state == LockState.STUCK:
|
|
2447
|
+
rule.locks_stuck_cnt -= 1
|
|
2448
|
+
account_counter.decrease(rse_id=rse_id, account=rule.account, files=1, bytes_=lock.bytes, session=session)
|
|
2449
|
+
if rule.state == RuleState.SUSPENDED:
|
|
2450
|
+
pass
|
|
2451
|
+
elif rule.state == RuleState.STUCK:
|
|
2452
|
+
pass
|
|
2453
|
+
elif rule.locks_replicating_cnt == 0 and rule.locks_stuck_cnt == 0:
|
|
2454
|
+
rule.state = RuleState.OK
|
|
2455
|
+
if rule.grouping != RuleGrouping.NONE:
|
|
2456
|
+
stmt = update(
|
|
2457
|
+
models.DatasetLock
|
|
2458
|
+
).where(
|
|
2459
|
+
models.DatasetLock.rule_id == rule.id
|
|
2460
|
+
).values({
|
|
2461
|
+
models.DatasetLock.state: LockState.OK
|
|
2462
|
+
})
|
|
2463
|
+
session.execute(stmt)
|
|
2464
|
+
session.flush()
|
|
2465
|
+
if rule_state_before != RuleState.OK:
|
|
2466
|
+
generate_rule_notifications(rule=rule, session=session)
|
|
2467
|
+
generate_email_for_rule_ok_notification(rule=rule, session=session)
|
|
2468
|
+
# Try to release potential parent rules
|
|
2469
|
+
release_parent_rule(child_rule_id=rule.id, session=session)
|
|
2470
|
+
# Insert rule history
|
|
2471
|
+
insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
|
|
2472
|
+
|
|
2473
|
+
session.delete(lock)
|
|
2474
|
+
|
|
2475
|
+
if replica.lock_cnt != 0:
|
|
2476
|
+
logger(logging.ERROR, 'Replica for did %s:%s with lock_cnt = %s. This should never happen. Update lock_cnt', scope, name, replica.lock_cnt)
|
|
2477
|
+
replica.lock_cnt = 0
|
|
2478
|
+
|
|
2479
|
+
replica.tombstone = OBSOLETE
|
|
2480
|
+
replica.state = ReplicaState.UNAVAILABLE
|
|
2481
|
+
stmt = update(
|
|
2482
|
+
models.DataIdentifier
|
|
2483
|
+
).where(
|
|
2484
|
+
and_(models.DataIdentifier.scope == scope,
|
|
2485
|
+
models.DataIdentifier.name == name)
|
|
2486
|
+
).values({
|
|
2487
|
+
models.DataIdentifier.availability: DIDAvailability.LOST
|
|
2488
|
+
})
|
|
2489
|
+
session.execute(stmt)
|
|
2490
|
+
|
|
2491
|
+
stmt = update(
|
|
2492
|
+
models.BadReplica
|
|
2493
|
+
).where(
|
|
2494
|
+
and_(models.BadReplica.scope == scope,
|
|
2495
|
+
models.BadReplica.name == name,
|
|
2496
|
+
models.BadReplica.rse_id == rse_id,
|
|
2497
|
+
models.BadReplica.state == BadFilesStatus.BAD)
|
|
2498
|
+
).values({
|
|
2499
|
+
models.BadReplica.state: BadFilesStatus.LOST,
|
|
2500
|
+
models.BadReplica.updated_at: datetime.utcnow()
|
|
2501
|
+
})
|
|
2502
|
+
session.execute(stmt)
|
|
2503
|
+
for dts in datasets:
|
|
2504
|
+
logger(logging.INFO, 'File %s:%s bad at site %s is completely lost from dataset %s:%s. Will be marked as LOST and detached', scope, name, rse, dts['scope'], dts['name'])
|
|
2505
|
+
rucio.core.did.detach_dids(scope=dts['scope'], name=dts['name'], dids=[{'scope': scope, 'name': name}], session=session)
|
|
2506
|
+
|
|
2507
|
+
message = {'scope': scope.external,
|
|
2508
|
+
'name': name,
|
|
2509
|
+
'dataset_name': dts['name'],
|
|
2510
|
+
'dataset_scope': dts['scope'].external}
|
|
2511
|
+
if scope.vo != 'def':
|
|
2512
|
+
message['vo'] = scope.vo
|
|
2513
|
+
|
|
2514
|
+
add_message('LOST', message, session=session)
|
|
2515
|
+
|
|
2516
|
+
|
|
2517
|
+
@transactional_session
|
|
2518
|
+
def update_rules_for_bad_replica(
|
|
2519
|
+
scope: InternalScope,
|
|
2520
|
+
name: str,
|
|
2521
|
+
rse_id: str,
|
|
2522
|
+
nowait: bool = False,
|
|
2523
|
+
*,
|
|
2524
|
+
session: "Session",
|
|
2525
|
+
logger: LoggerFunction = logging.log
|
|
2526
|
+
) -> None:
|
|
2527
|
+
"""
|
|
2528
|
+
Update rules if a file replica is bad and has to be recreated.
|
|
2529
|
+
|
|
2530
|
+
:param scope: Scope of the replica.
|
|
2531
|
+
:param name: Name of the replica.
|
|
2532
|
+
:param rse_id: RSE id of the replica.
|
|
2533
|
+
:param nowait: Nowait parameter for the FOR UPDATE statement.
|
|
2534
|
+
:param session: The database session in use.
|
|
2535
|
+
:param logger: Optional decorated logger that can be passed from the calling daemons or servers.
|
|
2536
|
+
"""
|
|
2537
|
+
stmt = select(
|
|
2538
|
+
models.ReplicaLock
|
|
2539
|
+
).where(
|
|
2540
|
+
and_(models.ReplicaLock.scope == scope,
|
|
2541
|
+
models.ReplicaLock.name == name,
|
|
2542
|
+
models.ReplicaLock.rse_id == rse_id)
|
|
2543
|
+
).with_for_update(
|
|
2544
|
+
nowait=nowait
|
|
2545
|
+
)
|
|
2546
|
+
locks = session.execute(stmt).scalars().all()
|
|
2547
|
+
|
|
2548
|
+
stmt = select(
|
|
2549
|
+
models.RSEFileAssociation
|
|
2550
|
+
).where(
|
|
2551
|
+
and_(models.RSEFileAssociation.scope == scope,
|
|
2552
|
+
models.RSEFileAssociation.name == name,
|
|
2553
|
+
models.RSEFileAssociation.rse_id == rse_id)
|
|
2554
|
+
).with_for_update(
|
|
2555
|
+
nowait=nowait
|
|
2556
|
+
)
|
|
2557
|
+
replica = session.execute(stmt).scalar_one()
|
|
2558
|
+
|
|
2559
|
+
nlock = 0
|
|
2560
|
+
datasets = []
|
|
2561
|
+
for lock in locks:
|
|
2562
|
+
nlock += 1
|
|
2563
|
+
stmt = select(
|
|
2564
|
+
models.ReplicationRule
|
|
2565
|
+
).where(
|
|
2566
|
+
models.ReplicationRule.id == lock.rule_id
|
|
2567
|
+
).with_for_update(
|
|
2568
|
+
nowait=nowait
|
|
2569
|
+
)
|
|
2570
|
+
rule = session.execute(stmt).scalar_one()
|
|
2571
|
+
# If source replica expression exists, we remove it
|
|
2572
|
+
if rule.source_replica_expression:
|
|
2573
|
+
rule.source_replica_expression = None
|
|
2574
|
+
# Get the affected datasets
|
|
2575
|
+
ds_scope = rule.scope
|
|
2576
|
+
ds_name = rule.name
|
|
2577
|
+
dataset = '%s:%s' % (ds_scope, ds_name)
|
|
2578
|
+
if dataset not in datasets:
|
|
2579
|
+
datasets.append(dataset)
|
|
2580
|
+
logger(logging.INFO, 'Recovering file %s:%s from dataset %s:%s at site %s', scope, name, ds_scope, ds_name, get_rse_name(rse_id=rse_id, session=session))
|
|
2581
|
+
# Insert a new row in the UpdateCollectionReplica table
|
|
2582
|
+
models.UpdatedCollectionReplica(scope=ds_scope,
|
|
2583
|
+
name=ds_name,
|
|
2584
|
+
did_type=rule.did_type,
|
|
2585
|
+
rse_id=lock.rse_id).save(flush=False, session=session)
|
|
2586
|
+
# Set the lock counters
|
|
2587
|
+
if lock.state == LockState.OK:
|
|
2588
|
+
rule.locks_ok_cnt -= 1
|
|
2589
|
+
elif lock.state == LockState.REPLICATING:
|
|
2590
|
+
rule.locks_replicating_cnt -= 1
|
|
2591
|
+
elif lock.state == LockState.STUCK:
|
|
2592
|
+
rule.locks_stuck_cnt -= 1
|
|
2593
|
+
rule.locks_replicating_cnt += 1
|
|
2594
|
+
# Generate the request
|
|
2595
|
+
try:
|
|
2596
|
+
request_core.get_request_by_did(scope, name, rse_id, session=session)
|
|
2597
|
+
except RequestNotFound:
|
|
2598
|
+
bytes_ = replica.bytes
|
|
2599
|
+
md5 = replica.md5
|
|
2600
|
+
adler32 = replica.adler32
|
|
2601
|
+
request_core.queue_requests(requests=[create_transfer_dict(dest_rse_id=rse_id,
|
|
2602
|
+
request_type=RequestType.TRANSFER,
|
|
2603
|
+
scope=scope, name=name, rule=rule, lock=lock, bytes_=bytes_, md5=md5, adler32=adler32,
|
|
2604
|
+
ds_scope=ds_scope, ds_name=ds_name, copy_pin_lifetime=None, activity='Recovery', session=session)], session=session)
|
|
2605
|
+
lock.state = LockState.REPLICATING
|
|
2606
|
+
if rule.state == RuleState.SUSPENDED:
|
|
2607
|
+
pass
|
|
2608
|
+
elif rule.state == RuleState.STUCK:
|
|
2609
|
+
pass
|
|
2610
|
+
else:
|
|
2611
|
+
rule.state = RuleState.REPLICATING
|
|
2612
|
+
if rule.grouping != RuleGrouping.NONE:
|
|
2613
|
+
stmt = update(
|
|
2614
|
+
models.DatasetLock
|
|
2615
|
+
).where(
|
|
2616
|
+
models.DatasetLock.rule_id == rule.id
|
|
2617
|
+
).values({
|
|
2618
|
+
models.DatasetLock.state: LockState.REPLICATING
|
|
2619
|
+
})
|
|
2620
|
+
session.execute(stmt)
|
|
2621
|
+
# Insert rule history
|
|
2622
|
+
insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
|
|
2623
|
+
if nlock:
|
|
2624
|
+
stmt = update(
|
|
2625
|
+
models.RSEFileAssociation
|
|
2626
|
+
).where(
|
|
2627
|
+
and_(models.RSEFileAssociation.scope == scope,
|
|
2628
|
+
models.RSEFileAssociation.name == name,
|
|
2629
|
+
models.RSEFileAssociation.rse_id == rse_id)
|
|
2630
|
+
).values({
|
|
2631
|
+
models.RSEFileAssociation.state: ReplicaState.COPYING
|
|
2632
|
+
})
|
|
2633
|
+
session.execute(stmt)
|
|
2634
|
+
else:
|
|
2635
|
+
logger(logging.INFO, 'File %s:%s at site %s has no locks. Will be deleted now.', scope, name, get_rse_name(rse_id=rse_id, session=session))
|
|
2636
|
+
tombstone = OBSOLETE
|
|
2637
|
+
stmt = update(
|
|
2638
|
+
models.RSEFileAssociation
|
|
2639
|
+
).where(
|
|
2640
|
+
and_(models.RSEFileAssociation.scope == scope,
|
|
2641
|
+
models.RSEFileAssociation.name == name,
|
|
2642
|
+
models.RSEFileAssociation.rse_id == rse_id)
|
|
2643
|
+
).values({
|
|
2644
|
+
models.RSEFileAssociation.state: ReplicaState.UNAVAILABLE,
|
|
2645
|
+
models.RSEFileAssociation.tombstone: tombstone
|
|
2646
|
+
})
|
|
2647
|
+
session.execute(stmt)
|
|
2648
|
+
|
|
2649
|
+
|
|
2650
|
+
@transactional_session
|
|
2651
|
+
def generate_rule_notifications(
|
|
2652
|
+
rule: models.ReplicationRule,
|
|
2653
|
+
replicating_locks_before: Optional[int] = None,
|
|
2654
|
+
*,
|
|
2655
|
+
session: "Session"
|
|
2656
|
+
) -> None:
|
|
2657
|
+
"""
|
|
2658
|
+
Generate (If necessary) a callback for a rule (DATASETLOCK_OK, RULE_OK, DATASETLOCK_PROGRESS)
|
|
2659
|
+
|
|
2660
|
+
:param rule: The rule object.
|
|
2661
|
+
:param replicating_locks_before: Amount of replicating locks before the current state change.
|
|
2662
|
+
:param session: The Database session
|
|
2663
|
+
"""
|
|
2664
|
+
|
|
2665
|
+
session.flush()
|
|
2666
|
+
total_locks = rule.locks_replicating_cnt + rule.locks_ok_cnt
|
|
2667
|
+
|
|
2668
|
+
if rule.state == RuleState.OK:
|
|
2669
|
+
# Only notify when rule is in state OK
|
|
2670
|
+
|
|
2671
|
+
# RULE_OK RULE_PROGRESS NOTIFICATIONS:
|
|
2672
|
+
if rule.notification == RuleNotification.YES:
|
|
2673
|
+
payload = {'scope': rule.scope.external,
|
|
2674
|
+
'name': rule.name,
|
|
2675
|
+
'rule_id': rule.id}
|
|
2676
|
+
if rule.scope.vo != 'def':
|
|
2677
|
+
payload['vo'] = rule.scope.vo
|
|
2678
|
+
|
|
2679
|
+
add_message(event_type='RULE_OK', payload=payload, session=session)
|
|
2680
|
+
|
|
2681
|
+
elif rule.notification in [RuleNotification.CLOSE, RuleNotification.PROGRESS]:
|
|
2682
|
+
try:
|
|
2683
|
+
did = rucio.core.did.get_did(scope=rule.scope, name=rule.name, session=session)
|
|
2684
|
+
if not did['open']:
|
|
2685
|
+
payload = {'scope': rule.scope.external,
|
|
2686
|
+
'name': rule.name,
|
|
2687
|
+
'rule_id': rule.id}
|
|
2688
|
+
if rule.scope.vo != 'def':
|
|
2689
|
+
payload['vo'] = rule.scope.vo
|
|
2690
|
+
|
|
2691
|
+
add_message(event_type='RULE_OK', payload=payload, session=session)
|
|
2692
|
+
|
|
2693
|
+
if rule.notification == RuleNotification.PROGRESS:
|
|
2694
|
+
payload = {'scope': rule.scope.external,
|
|
2695
|
+
'name': rule.name,
|
|
2696
|
+
'rule_id': rule.id,
|
|
2697
|
+
'progress': __progress_class(rule.locks_replicating_cnt, total_locks)}
|
|
2698
|
+
if rule.scope.vo != 'def':
|
|
2699
|
+
payload['vo'] = rule.scope.vo
|
|
2700
|
+
|
|
2701
|
+
add_message(event_type='RULE_PROGRESS', payload=payload, session=session)
|
|
2702
|
+
|
|
2703
|
+
except DataIdentifierNotFound:
|
|
2704
|
+
pass
|
|
2705
|
+
|
|
2706
|
+
# DATASETLOCK_OK NOTIFICATIONS:
|
|
2707
|
+
if rule.grouping != RuleGrouping.NONE:
|
|
2708
|
+
# Only send DATASETLOCK_OK callbacks for ALL/DATASET grouped rules
|
|
2709
|
+
if rule.notification == RuleNotification.YES:
|
|
2710
|
+
stmt = select(
|
|
2711
|
+
models.DatasetLock
|
|
2712
|
+
).where(
|
|
2713
|
+
models.DatasetLock.rule_id == rule.id
|
|
2714
|
+
)
|
|
2715
|
+
dataset_locks = session.execute(stmt).scalars().all()
|
|
2716
|
+
for dataset_lock in dataset_locks:
|
|
2717
|
+
payload = {'scope': dataset_lock.scope.external,
|
|
2718
|
+
'name': dataset_lock.name,
|
|
2719
|
+
'rse': get_rse_name(rse_id=dataset_lock.rse_id, session=session),
|
|
2720
|
+
'rse_id': dataset_lock.rse_id,
|
|
2721
|
+
'rule_id': rule.id}
|
|
2722
|
+
if dataset_lock.scope.vo != 'def':
|
|
2723
|
+
payload['vo'] = dataset_lock.scope.vo
|
|
2724
|
+
|
|
2725
|
+
add_message(event_type='DATASETLOCK_OK', payload=payload, session=session)
|
|
2726
|
+
|
|
2727
|
+
elif rule.notification == RuleNotification.CLOSE:
|
|
2728
|
+
stmt = select(
|
|
2729
|
+
models.DatasetLock
|
|
2730
|
+
).where(
|
|
2731
|
+
models.DatasetLock.rule_id == rule.id
|
|
2732
|
+
)
|
|
2733
|
+
dataset_locks = session.execute(stmt).scalars().all()
|
|
2734
|
+
for dataset_lock in dataset_locks:
|
|
2735
|
+
try:
|
|
2736
|
+
did = rucio.core.did.get_did(scope=dataset_lock.scope, name=dataset_lock.name, session=session)
|
|
2737
|
+
if not did['open']:
|
|
2738
|
+
if did['length'] is None:
|
|
2739
|
+
return
|
|
2740
|
+
if did['length'] * rule.copies == rule.locks_ok_cnt:
|
|
2741
|
+
payload = {'scope': dataset_lock.scope.external,
|
|
2742
|
+
'name': dataset_lock.name,
|
|
2743
|
+
'rse': get_rse_name(rse_id=dataset_lock.rse_id, session=session),
|
|
2744
|
+
'rse_id': dataset_lock.rse_id,
|
|
2745
|
+
'rule_id': rule.id}
|
|
2746
|
+
if dataset_lock.scope.vo != 'def':
|
|
2747
|
+
payload['vo'] = dataset_lock.scope.vo
|
|
2748
|
+
|
|
2749
|
+
add_message(event_type='DATASETLOCK_OK', payload=payload, session=session)
|
|
2750
|
+
|
|
2751
|
+
except DataIdentifierNotFound:
|
|
2752
|
+
pass
|
|
2753
|
+
|
|
2754
|
+
elif rule.state == RuleState.REPLICATING and rule.notification == RuleNotification.PROGRESS and replicating_locks_before:
|
|
2755
|
+
# For RuleNotification PROGRESS rules, also notify when REPLICATING thresholds are passed
|
|
2756
|
+
if __progress_class(replicating_locks_before, total_locks) != __progress_class(rule.locks_replicating_cnt, total_locks):
|
|
2757
|
+
try:
|
|
2758
|
+
did = rucio.core.did.get_did(scope=rule.scope, name=rule.name, session=session)
|
|
2759
|
+
if not did['open']:
|
|
2760
|
+
payload = {'scope': rule.scope.external,
|
|
2761
|
+
'name': rule.name,
|
|
2762
|
+
'rule_id': rule.id,
|
|
2763
|
+
'progress': __progress_class(rule.locks_replicating_cnt, total_locks)}
|
|
2764
|
+
if rule.scope.vo != 'def':
|
|
2765
|
+
payload['vo'] = rule.scope.vo
|
|
2766
|
+
|
|
2767
|
+
add_message(event_type='RULE_PROGRESS', payload=payload, session=session)
|
|
2768
|
+
|
|
2769
|
+
except DataIdentifierNotFound:
|
|
2770
|
+
pass
|
|
2771
|
+
|
|
2772
|
+
|
|
2773
|
+
@transactional_session
|
|
2774
|
+
def generate_email_for_rule_ok_notification(
|
|
2775
|
+
rule: models.ReplicationRule,
|
|
2776
|
+
*,
|
|
2777
|
+
session: "Session",
|
|
2778
|
+
logger: LoggerFunction = logging.log
|
|
2779
|
+
) -> None:
|
|
2780
|
+
"""
|
|
2781
|
+
Generate (If necessary) an eMail for a rule with notification mode Y.
|
|
2782
|
+
|
|
2783
|
+
:param rule: The rule object.
|
|
2784
|
+
:param session: The Database session
|
|
2785
|
+
:param logger: Optional decorated logger that can be passed from the calling daemons or servers.
|
|
2786
|
+
"""
|
|
2787
|
+
|
|
2788
|
+
session.flush()
|
|
2789
|
+
|
|
2790
|
+
if rule.state == RuleState.OK and rule.notification == RuleNotification.YES:
|
|
2791
|
+
try:
|
|
2792
|
+
template_path = '%s/rule_ok_notification.tmpl' % config_get('common', 'mailtemplatedir')
|
|
2793
|
+
except NoOptionError as ex:
|
|
2794
|
+
logger(logging.ERROR, "Missing configuration option 'mailtemplatedir'.", exc_info=ex)
|
|
2795
|
+
return
|
|
2796
|
+
|
|
2797
|
+
try:
|
|
2798
|
+
with open(template_path, 'r') as templatefile:
|
|
2799
|
+
template = Template(templatefile.read())
|
|
2800
|
+
except OSError as ex:
|
|
2801
|
+
logger(logging.ERROR, "Couldn't open file '%s'", template_path, exc_info=ex)
|
|
2802
|
+
return
|
|
2803
|
+
|
|
2804
|
+
email = get_account(account=rule.account, session=session).email
|
|
2805
|
+
if not email:
|
|
2806
|
+
logger(logging.INFO, 'No email associated with rule ID %s.', rule.id)
|
|
2807
|
+
return
|
|
2808
|
+
|
|
2809
|
+
try:
|
|
2810
|
+
email_body = template.safe_substitute({'rule_id': str(rule.id),
|
|
2811
|
+
'created_at': str(rule.created_at),
|
|
2812
|
+
'expires_at': str(rule.expires_at),
|
|
2813
|
+
'rse_expression': rule.rse_expression,
|
|
2814
|
+
'comment': rule.comments,
|
|
2815
|
+
'scope': rule.scope.external,
|
|
2816
|
+
'name': rule.name,
|
|
2817
|
+
'did_type': rule.did_type})
|
|
2818
|
+
except ValueError as ex:
|
|
2819
|
+
logger(logging.ERROR, "Invalid mail template.", exc_info=ex)
|
|
2820
|
+
return
|
|
2821
|
+
|
|
2822
|
+
add_message(event_type='email',
|
|
2823
|
+
payload={'body': email_body,
|
|
2824
|
+
'to': [email],
|
|
2825
|
+
'subject': '[RUCIO] Replication rule %s has been successfully transferred' % (str(rule.id))},
|
|
2826
|
+
session=session)
|
|
2827
|
+
|
|
2828
|
+
|
|
2829
|
+
@transactional_session
|
|
2830
|
+
def insert_rule_history(
|
|
2831
|
+
rule: models.ReplicationRule,
|
|
2832
|
+
recent: bool = True,
|
|
2833
|
+
longterm: bool = False,
|
|
2834
|
+
*,
|
|
2835
|
+
session: "Session"
|
|
2836
|
+
) -> None:
|
|
2837
|
+
"""
|
|
2838
|
+
Insert rule history to recent/longterm history.
|
|
2839
|
+
|
|
2840
|
+
:param rule: The rule object.
|
|
2841
|
+
:param recent: Insert to recent table.
|
|
2842
|
+
:param longterm: Insert to longterm table.
|
|
2843
|
+
:param session: The Database session.
|
|
2844
|
+
"""
|
|
2845
|
+
if recent:
|
|
2846
|
+
models.ReplicationRuleHistoryRecent(id=rule.id, subscription_id=rule.subscription_id, account=rule.account, scope=rule.scope, name=rule.name,
|
|
2847
|
+
did_type=rule.did_type, state=rule.state, error=rule.error, rse_expression=rule.rse_expression, copies=rule.copies,
|
|
2848
|
+
expires_at=rule.expires_at, weight=rule.weight, locked=rule.locked, locks_ok_cnt=rule.locks_ok_cnt,
|
|
2849
|
+
locks_replicating_cnt=rule.locks_replicating_cnt, locks_stuck_cnt=rule.locks_stuck_cnt, source_replica_expression=rule.source_replica_expression,
|
|
2850
|
+
activity=rule.activity, grouping=rule.grouping, notification=rule.notification, stuck_at=rule.stuck_at, purge_replicas=rule.purge_replicas,
|
|
2851
|
+
ignore_availability=rule.ignore_availability, ignore_account_limit=rule.ignore_account_limit, comments=rule.comments, created_at=rule.created_at,
|
|
2852
|
+
updated_at=rule.updated_at, child_rule_id=rule.child_rule_id, eol_at=rule.eol_at,
|
|
2853
|
+
split_container=rule.split_container, meta=rule.meta).save(session=session)
|
|
2854
|
+
if longterm:
|
|
2855
|
+
models.ReplicationRuleHistory(id=rule.id, subscription_id=rule.subscription_id, account=rule.account, scope=rule.scope, name=rule.name,
|
|
2856
|
+
did_type=rule.did_type, state=rule.state, error=rule.error, rse_expression=rule.rse_expression, copies=rule.copies,
|
|
2857
|
+
expires_at=rule.expires_at, weight=rule.weight, locked=rule.locked, locks_ok_cnt=rule.locks_ok_cnt,
|
|
2858
|
+
locks_replicating_cnt=rule.locks_replicating_cnt, locks_stuck_cnt=rule.locks_stuck_cnt, source_replica_expression=rule.source_replica_expression,
|
|
2859
|
+
activity=rule.activity, grouping=rule.grouping, notification=rule.notification, stuck_at=rule.stuck_at, purge_replicas=rule.purge_replicas,
|
|
2860
|
+
ignore_availability=rule.ignore_availability, ignore_account_limit=rule.ignore_account_limit, comments=rule.comments, created_at=rule.created_at,
|
|
2861
|
+
updated_at=rule.updated_at, child_rule_id=rule.child_rule_id, eol_at=rule.eol_at,
|
|
2862
|
+
split_container=rule.split_container, meta=rule.meta).save(session=session)
|
|
2863
|
+
|
|
2864
|
+
|
|
2865
|
+
@transactional_session
|
|
2866
|
+
def approve_rule(
|
|
2867
|
+
rule_id: str,
|
|
2868
|
+
approver: str = '',
|
|
2869
|
+
notify_approvers: bool = True,
|
|
2870
|
+
*,
|
|
2871
|
+
session: "Session"
|
|
2872
|
+
) -> None:
|
|
2873
|
+
"""
|
|
2874
|
+
Approve a specific replication rule.
|
|
2875
|
+
|
|
2876
|
+
:param rule_id: The rule_id to approve.
|
|
2877
|
+
:param approver: The account which is approving the rule.
|
|
2878
|
+
:param notify_approvers: Notify the other approvers.
|
|
2879
|
+
:param session: The database session in use.
|
|
2880
|
+
:raises: RuleNotFound if no Rule can be found.
|
|
2881
|
+
"""
|
|
2882
|
+
|
|
2883
|
+
try:
|
|
2884
|
+
stmt = select(
|
|
2885
|
+
models.ReplicationRule
|
|
2886
|
+
).where(
|
|
2887
|
+
models.ReplicationRule.id == rule_id
|
|
2888
|
+
)
|
|
2889
|
+
rule = session.execute(stmt).scalar_one()
|
|
2890
|
+
if rule.state == RuleState.WAITING_APPROVAL:
|
|
2891
|
+
rule.ignore_account_limit = True
|
|
2892
|
+
rule.state = RuleState.INJECT
|
|
2893
|
+
if approver:
|
|
2894
|
+
approver_email = get_account(account=approver, session=session).email
|
|
2895
|
+
if approver_email:
|
|
2896
|
+
approver = '%s (%s)' % (approver, approver_email)
|
|
2897
|
+
else:
|
|
2898
|
+
approver = 'AUTOMATIC'
|
|
2899
|
+
with open('%s/rule_approved_user.tmpl' % config_get('common', 'mailtemplatedir'), 'r') as templatefile:
|
|
2900
|
+
template = Template(templatefile.read())
|
|
2901
|
+
email = get_account(account=rule.account, session=session).email
|
|
2902
|
+
if email:
|
|
2903
|
+
text = template.safe_substitute({'rule_id': str(rule.id),
|
|
2904
|
+
'expires_at': str(rule.expires_at),
|
|
2905
|
+
'rse_expression': rule.rse_expression,
|
|
2906
|
+
'comment': rule.comments,
|
|
2907
|
+
'scope': rule.scope.external,
|
|
2908
|
+
'name': rule.name,
|
|
2909
|
+
'did_type': rule.did_type,
|
|
2910
|
+
'approver': approver})
|
|
2911
|
+
add_message(event_type='email',
|
|
2912
|
+
payload={'body': text,
|
|
2913
|
+
'to': [email],
|
|
2914
|
+
'subject': '[RUCIO] Replication rule %s has been approved' % (str(rule.id))},
|
|
2915
|
+
session=session)
|
|
2916
|
+
# Also notify the other approvers
|
|
2917
|
+
if notify_approvers:
|
|
2918
|
+
with open('%s/rule_approved_admin.tmpl' % config_get('common', 'mailtemplatedir'), 'r') as templatefile:
|
|
2919
|
+
template = Template(templatefile.read())
|
|
2920
|
+
text = template.safe_substitute({'rule_id': str(rule.id),
|
|
2921
|
+
'approver': approver})
|
|
2922
|
+
vo = rule.account.vo
|
|
2923
|
+
recipients = _create_recipients_list(rse_expression=rule.rse_expression, filter_={'vo': vo}, session=session)
|
|
2924
|
+
for recipient in recipients:
|
|
2925
|
+
add_message(event_type='email',
|
|
2926
|
+
payload={'body': text,
|
|
2927
|
+
'to': [recipient[0]],
|
|
2928
|
+
'subject': 'Re: [RUCIO] Request to approve replication rule %s' % (str(rule.id))},
|
|
2929
|
+
session=session)
|
|
2930
|
+
except NoResultFound as exc:
|
|
2931
|
+
raise RuleNotFound('No rule with the id %s found' % (rule_id)) from exc
|
|
2932
|
+
except StatementError as exc:
|
|
2933
|
+
raise RucioException('Badly formatted rule id (%s)' % (rule_id)) from exc
|
|
2934
|
+
|
|
2935
|
+
|
|
2936
|
+
@transactional_session
|
|
2937
|
+
def deny_rule(
|
|
2938
|
+
rule_id: str,
|
|
2939
|
+
approver: str = '',
|
|
2940
|
+
reason: Optional[str] = None,
|
|
2941
|
+
*,
|
|
2942
|
+
session: "Session"
|
|
2943
|
+
) -> None:
|
|
2944
|
+
"""
|
|
2945
|
+
Deny a specific replication rule.
|
|
2946
|
+
|
|
2947
|
+
:param rule_id: The rule_id to approve.
|
|
2948
|
+
:param approver: The account which is denying the rule.
|
|
2949
|
+
:param reason: The reason why the rule was denied.
|
|
2950
|
+
:param session: The database session in use.
|
|
2951
|
+
:raises: RuleNotFound if no Rule can be found.
|
|
2952
|
+
"""
|
|
2953
|
+
|
|
2954
|
+
try:
|
|
2955
|
+
stmt = select(
|
|
2956
|
+
models.ReplicationRule
|
|
2957
|
+
).where(
|
|
2958
|
+
models.ReplicationRule.id == rule_id
|
|
2959
|
+
)
|
|
2960
|
+
rule = session.execute(stmt).scalar_one()
|
|
2961
|
+
if rule.state == RuleState.WAITING_APPROVAL:
|
|
2962
|
+
with open('%s/rule_denied_user.tmpl' % config_get('common', 'mailtemplatedir'), 'r') as templatefile:
|
|
2963
|
+
template = Template(templatefile.read())
|
|
2964
|
+
email = get_account(account=rule.account, session=session).email
|
|
2965
|
+
if approver:
|
|
2966
|
+
approver_email = get_account(account=approver, session=session).email
|
|
2967
|
+
if approver_email:
|
|
2968
|
+
approver = '%s (%s)' % (approver, approver_email)
|
|
2969
|
+
else:
|
|
2970
|
+
approver = 'AUTOMATIC'
|
|
2971
|
+
if email:
|
|
2972
|
+
email_body = template.safe_substitute({'rule_id': str(rule.id),
|
|
2973
|
+
'rse_expression': rule.rse_expression,
|
|
2974
|
+
'comment': rule.comments,
|
|
2975
|
+
'scope': rule.scope.external,
|
|
2976
|
+
'name': rule.name,
|
|
2977
|
+
'did_type': rule.did_type,
|
|
2978
|
+
'approver': approver,
|
|
2979
|
+
'reason': reason})
|
|
2980
|
+
add_message(event_type='email',
|
|
2981
|
+
payload={'body': email_body,
|
|
2982
|
+
'to': [email],
|
|
2983
|
+
'subject': '[RUCIO] Replication rule %s has been denied' % (str(rule.id))},
|
|
2984
|
+
session=session)
|
|
2985
|
+
delete_rule(rule_id=rule_id, ignore_rule_lock=True, session=session)
|
|
2986
|
+
# Also notify the other approvers
|
|
2987
|
+
with open('%s/rule_denied_admin.tmpl' % config_get('common', 'mailtemplatedir'), 'r') as templatefile:
|
|
2988
|
+
template = Template(templatefile.read())
|
|
2989
|
+
email_body = template.safe_substitute({'rule_id': str(rule.id),
|
|
2990
|
+
'approver': approver,
|
|
2991
|
+
'reason': reason})
|
|
2992
|
+
vo = rule.account.vo
|
|
2993
|
+
recipients = _create_recipients_list(rse_expression=rule.rse_expression, filter_={'vo': vo}, session=session)
|
|
2994
|
+
for recipient in recipients:
|
|
2995
|
+
add_message(event_type='email',
|
|
2996
|
+
payload={'body': email_body,
|
|
2997
|
+
'to': [recipient[0]],
|
|
2998
|
+
'subject': 'Re: [RUCIO] Request to approve replication rule %s' % (str(rule.id))},
|
|
2999
|
+
session=session)
|
|
3000
|
+
except NoResultFound as exc:
|
|
3001
|
+
raise RuleNotFound('No rule with the id %s found' % rule_id) from exc
|
|
3002
|
+
except StatementError as exc:
|
|
3003
|
+
raise RucioException('Badly formatted rule id (%s)' % rule_id) from exc
|
|
3004
|
+
|
|
3005
|
+
|
|
3006
|
+
@transactional_session
|
|
3007
|
+
def examine_rule(
|
|
3008
|
+
rule_id: str,
|
|
3009
|
+
*,
|
|
3010
|
+
session: "Session"
|
|
3011
|
+
) -> dict[str, Any]:
|
|
3012
|
+
"""
|
|
3013
|
+
Examine a replication rule for transfer errors.
|
|
3014
|
+
|
|
3015
|
+
:param rule_id: Replication rule id
|
|
3016
|
+
:param session: Session of the db.
|
|
3017
|
+
:returns: Dictionary of information
|
|
3018
|
+
"""
|
|
3019
|
+
result = {'rule_error': None,
|
|
3020
|
+
'transfers': []}
|
|
3021
|
+
|
|
3022
|
+
try:
|
|
3023
|
+
stmt = select(
|
|
3024
|
+
models.ReplicationRule
|
|
3025
|
+
).where(
|
|
3026
|
+
models.ReplicationRule.id == rule_id
|
|
3027
|
+
)
|
|
3028
|
+
rule = session.execute(stmt).scalar_one()
|
|
3029
|
+
if rule.state == RuleState.OK:
|
|
3030
|
+
result['rule_error'] = 'This replication rule is OK'
|
|
3031
|
+
elif rule.state == RuleState.REPLICATING:
|
|
3032
|
+
result['rule_error'] = 'This replication rule is currently REPLICATING'
|
|
3033
|
+
elif rule.state == RuleState.SUSPENDED:
|
|
3034
|
+
result['rule_error'] = 'This replication rule is SUSPENDED'
|
|
3035
|
+
else:
|
|
3036
|
+
result['rule_error'] = rule.error
|
|
3037
|
+
# Get the stuck locks
|
|
3038
|
+
stmt = select(
|
|
3039
|
+
models.ReplicaLock
|
|
3040
|
+
).where(
|
|
3041
|
+
and_(models.ReplicaLock.rule_id == rule_id,
|
|
3042
|
+
models.ReplicaLock.state == LockState.STUCK)
|
|
3043
|
+
)
|
|
3044
|
+
stuck_locks = session.execute(stmt).scalars().all()
|
|
3045
|
+
for lock in stuck_locks:
|
|
3046
|
+
# Get the count of requests in the request_history for each lock
|
|
3047
|
+
stmt = select(
|
|
3048
|
+
models.RequestHistory
|
|
3049
|
+
).where(
|
|
3050
|
+
and_(models.RequestHistory.scope == lock.scope,
|
|
3051
|
+
models.RequestHistory.name == lock.name,
|
|
3052
|
+
models.RequestHistory.dest_rse_id == lock.rse_id)
|
|
3053
|
+
).order_by(
|
|
3054
|
+
desc(models.RequestHistory.created_at)
|
|
3055
|
+
)
|
|
3056
|
+
transfers = session.execute(stmt).scalars().all()
|
|
3057
|
+
transfer_cnt = len(transfers)
|
|
3058
|
+
# Get the error of the last request that has been tried and also the SOURCE used for the last request
|
|
3059
|
+
last_error, last_source, last_time, sources = None, None, None, []
|
|
3060
|
+
if transfers:
|
|
3061
|
+
last_request = transfers[0]
|
|
3062
|
+
last_error = last_request.state
|
|
3063
|
+
last_time = last_request.created_at
|
|
3064
|
+
last_source = None if last_request.source_rse_id is None else get_rse_name(rse_id=last_request.source_rse_id, session=session)
|
|
3065
|
+
stmt = select(
|
|
3066
|
+
models.RSEFileAssociation
|
|
3067
|
+
).where(
|
|
3068
|
+
and_(models.RSEFileAssociation.scope == lock.scope,
|
|
3069
|
+
models.RSEFileAssociation.name == lock.name,
|
|
3070
|
+
models.RSEFileAssociation.state == ReplicaState.AVAILABLE)
|
|
3071
|
+
)
|
|
3072
|
+
available_replicas = session.execute(stmt).scalars().all()
|
|
3073
|
+
|
|
3074
|
+
for replica in available_replicas:
|
|
3075
|
+
sources.append((get_rse_name(rse_id=replica.rse_id, session=session),
|
|
3076
|
+
True if get_rse(rse_id=replica.rse_id, session=session)['availability_read'] else False))
|
|
3077
|
+
|
|
3078
|
+
result['transfers'].append({'scope': lock.scope,
|
|
3079
|
+
'name': lock.name,
|
|
3080
|
+
'rse_id': lock.rse_id,
|
|
3081
|
+
'rse': get_rse_name(rse_id=lock.rse_id, session=session),
|
|
3082
|
+
'attempts': transfer_cnt,
|
|
3083
|
+
'last_error': str(last_error),
|
|
3084
|
+
'last_source': last_source,
|
|
3085
|
+
'sources': sources,
|
|
3086
|
+
'last_time': last_time})
|
|
3087
|
+
return result
|
|
3088
|
+
except NoResultFound as exc:
|
|
3089
|
+
raise RuleNotFound('No rule with the id %s found' % (rule_id)) from exc
|
|
3090
|
+
except StatementError as exc:
|
|
3091
|
+
raise RucioException('Badly formatted rule id (%s)' % (rule_id)) from exc
|
|
3092
|
+
|
|
3093
|
+
|
|
3094
|
+
@transactional_session
|
|
3095
|
+
def get_evaluation_backlog(
|
|
3096
|
+
expiration_time: int = 600,
|
|
3097
|
+
*,
|
|
3098
|
+
session: "Session"
|
|
3099
|
+
) -> tuple[int, datetime]:
|
|
3100
|
+
"""
|
|
3101
|
+
Counts the number of entries in the rule evaluation backlog.
|
|
3102
|
+
(Number of files to be evaluated)
|
|
3103
|
+
|
|
3104
|
+
:returns: Tuple (Count, Datetime of oldest entry)
|
|
3105
|
+
"""
|
|
3106
|
+
|
|
3107
|
+
cached_backlog: Union[NoValue, tuple[int, datetime]] = REGION.get('rule_evaluation_backlog', expiration_time=expiration_time)
|
|
3108
|
+
if isinstance(cached_backlog, NoValue):
|
|
3109
|
+
stmt = select(
|
|
3110
|
+
func.count(models.UpdatedDID.created_at),
|
|
3111
|
+
func.min(models.UpdatedDID.created_at)
|
|
3112
|
+
)
|
|
3113
|
+
result = session.execute(stmt).one()._tuple()
|
|
3114
|
+
REGION.set('rule_evaluation_backlog', result)
|
|
3115
|
+
return result
|
|
3116
|
+
return cached_backlog
|
|
3117
|
+
|
|
3118
|
+
|
|
3119
|
+
@transactional_session
|
|
3120
|
+
def release_parent_rule(
|
|
3121
|
+
child_rule_id: str,
|
|
3122
|
+
remove_parent_expiration: bool = False,
|
|
3123
|
+
*,
|
|
3124
|
+
session: "Session"
|
|
3125
|
+
) -> None:
|
|
3126
|
+
"""
|
|
3127
|
+
Release a potential parent rule, because the child_rule is OK.
|
|
3128
|
+
|
|
3129
|
+
:param child_rule_id: The child rule id.
|
|
3130
|
+
:param remove_parant_expiration: If true, removes the expiration of the parent rule.
|
|
3131
|
+
:param session: The Database session
|
|
3132
|
+
"""
|
|
3133
|
+
|
|
3134
|
+
session.flush()
|
|
3135
|
+
|
|
3136
|
+
stmt = select(
|
|
3137
|
+
models.ReplicationRule
|
|
3138
|
+
).with_hint(
|
|
3139
|
+
models.ReplicationRule, 'INDEX(RULES RULES_CHILD_RULE_ID_IDX)', 'oracle'
|
|
3140
|
+
).where(
|
|
3141
|
+
models.ReplicationRule.child_rule_id == child_rule_id
|
|
3142
|
+
)
|
|
3143
|
+
parent_rules = session.execute(stmt).scalars().all()
|
|
3144
|
+
for rule in parent_rules:
|
|
3145
|
+
if remove_parent_expiration:
|
|
3146
|
+
rule.expires_at = None
|
|
3147
|
+
rule.child_rule_id = None
|
|
3148
|
+
insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
|
|
3149
|
+
|
|
3150
|
+
|
|
3151
|
+
@stream_session
|
|
3152
|
+
def list_rules_for_rse_decommissioning(
|
|
3153
|
+
rse_id: str,
|
|
3154
|
+
*,
|
|
3155
|
+
session: "Session"
|
|
3156
|
+
) -> 'Iterator[dict[str, Any]]':
|
|
3157
|
+
"""Return a generator of rules at the RSE that is being decommissioned.
|
|
3158
|
+
|
|
3159
|
+
Decommissioning of an RSE involves deleting or moving away all rules that are
|
|
3160
|
+
locking the replicas that exist at the RSE. The rules can be enforcing
|
|
3161
|
+
dataset-level and/or file-level locks. Because rules are defined in terms of
|
|
3162
|
+
RSE expressions, we need to first identify the locks with the RSE id and make
|
|
3163
|
+
the list of rules that are enforcing such locks.
|
|
3164
|
+
This function has two yield statements corresponding to the two types
|
|
3165
|
+
(dataset-level and file-level) of locks. To avoid listing duplicates, the
|
|
3166
|
+
rules identified through the dataset-level locks are excluded from the
|
|
3167
|
+
second query using file-level locks.
|
|
3168
|
+
|
|
3169
|
+
:param rse_id: Id of the RSE being decommissioned.
|
|
3170
|
+
:param session: The database session in use.
|
|
3171
|
+
:returns: A generator that yields rule dictionaries.
|
|
3172
|
+
"""
|
|
3173
|
+
# Get rules with dataset locks first.
|
|
3174
|
+
query_rules_from_dataset_locks = select(
|
|
3175
|
+
models.ReplicationRule
|
|
3176
|
+
).distinct(
|
|
3177
|
+
).join_from(
|
|
3178
|
+
models.DatasetLock,
|
|
3179
|
+
models.ReplicationRule,
|
|
3180
|
+
models.DatasetLock.rule_id == models.ReplicationRule.id
|
|
3181
|
+
).where(
|
|
3182
|
+
models.DatasetLock.rse_id == rse_id
|
|
3183
|
+
)
|
|
3184
|
+
|
|
3185
|
+
for rule in session.execute(query_rules_from_dataset_locks).yield_per(5).scalars():
|
|
3186
|
+
yield rule.to_dict()
|
|
3187
|
+
|
|
3188
|
+
# Make a subquery from the previous query to be excluded from the next query
|
|
3189
|
+
dataset_rule_ids = query_rules_from_dataset_locks.with_only_columns(models.ReplicationRule.id)
|
|
3190
|
+
|
|
3191
|
+
# ReplicaLock ("locks") table is not indexed by RSE ID, so we instead go
|
|
3192
|
+
# through the RSEFileAssociation ("replicas") table.
|
|
3193
|
+
query_rules_from_replicas = select(
|
|
3194
|
+
models.ReplicationRule
|
|
3195
|
+
).prefix_with(
|
|
3196
|
+
'/*+ USE_NL(locks) LEADING(replicas locks) */',
|
|
3197
|
+
dialect='oracle'
|
|
3198
|
+
).distinct(
|
|
3199
|
+
).join_from(
|
|
3200
|
+
models.RSEFileAssociation,
|
|
3201
|
+
models.ReplicaLock,
|
|
3202
|
+
and_(models.RSEFileAssociation.scope == models.ReplicaLock.scope,
|
|
3203
|
+
models.RSEFileAssociation.name == models.ReplicaLock.name,
|
|
3204
|
+
models.RSEFileAssociation.rse_id == models.ReplicaLock.rse_id)
|
|
3205
|
+
).join(
|
|
3206
|
+
models.ReplicationRule,
|
|
3207
|
+
models.ReplicaLock.rule_id == models.ReplicationRule.id
|
|
3208
|
+
).where(
|
|
3209
|
+
models.RSEFileAssociation.rse_id == rse_id,
|
|
3210
|
+
models.ReplicaLock.rule_id.not_in(dataset_rule_ids)
|
|
3211
|
+
)
|
|
3212
|
+
|
|
3213
|
+
for rule in session.execute(query_rules_from_replicas).yield_per(5).scalars():
|
|
3214
|
+
yield rule.to_dict()
|
|
3215
|
+
|
|
3216
|
+
|
|
3217
|
+
@transactional_session
|
|
3218
|
+
def __find_missing_locks_and_create_them(
|
|
3219
|
+
datasetfiles: 'Sequence[dict[str, Any]]',
|
|
3220
|
+
locks: dict[tuple[InternalScope, str], 'Sequence[models.ReplicaLock]'],
|
|
3221
|
+
replicas: dict[tuple[InternalScope, str], 'Sequence[models.CollectionReplica]'],
|
|
3222
|
+
source_replicas: dict[tuple[InternalScope, str], 'Sequence[models.CollectionReplica]'],
|
|
3223
|
+
rseselector: RSESelector,
|
|
3224
|
+
rule: models.ReplicationRule,
|
|
3225
|
+
source_rses: 'Sequence[str]',
|
|
3226
|
+
*,
|
|
3227
|
+
session: "Session",
|
|
3228
|
+
logger: LoggerFunction = logging.log
|
|
3229
|
+
) -> None:
|
|
3230
|
+
"""
|
|
3231
|
+
Find missing locks for a rule and create them.
|
|
3232
|
+
|
|
3233
|
+
:param datasetfiles: Sequence of dicts holding all datasets and files.
|
|
3234
|
+
:param locks: Dict holding locks.
|
|
3235
|
+
:param replicas: Dict holding replicas.
|
|
3236
|
+
:param source_replicas: Dict holding source replicas.
|
|
3237
|
+
:param rseselector: The RSESelector to be used.
|
|
3238
|
+
:param rule: The rule.
|
|
3239
|
+
:param source_rses: RSE ids for eligible source RSEs.
|
|
3240
|
+
:param session: Session of the db.
|
|
3241
|
+
:param logger: Optional decorated logger that can be passed from the calling daemons or servers.
|
|
3242
|
+
:raises: InsufficientAccountLimit, IntegrityError, InsufficientTargetRSEs
|
|
3243
|
+
:attention: This method modifies the contents of the locks and replicas input parameters.
|
|
3244
|
+
"""
|
|
3245
|
+
|
|
3246
|
+
logger(logging.DEBUG, "Finding missing locks for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
|
|
3247
|
+
|
|
3248
|
+
mod_datasetfiles = [] # List of Datasets and their files in the Tree [{'scope':, 'name':, 'files': []}]
|
|
3249
|
+
# Files are in the format [{'scope':, 'name':, 'bytes':, 'md5':, 'adler32':}]
|
|
3250
|
+
|
|
3251
|
+
for dataset in datasetfiles:
|
|
3252
|
+
mod_files = []
|
|
3253
|
+
preferred_rse_ids = []
|
|
3254
|
+
for file in dataset['files']:
|
|
3255
|
+
if len([lock for lock in locks[(file['scope'], file['name'])] if lock.rule_id == rule.id]) < rule.copies:
|
|
3256
|
+
mod_files.append(file)
|
|
3257
|
+
else:
|
|
3258
|
+
preferred_rse_ids = [lock.rse_id for lock in locks[(file['scope'], file['name'])] if lock.rule_id == rule.id]
|
|
3259
|
+
if mod_files:
|
|
3260
|
+
logger(logging.DEBUG, 'Found missing locks for rule %s, creating them now', str(rule.id))
|
|
3261
|
+
mod_datasetfiles.append({'scope': dataset['scope'], 'name': dataset['name'], 'files': mod_files})
|
|
3262
|
+
__create_locks_replicas_transfers(datasetfiles=mod_datasetfiles,
|
|
3263
|
+
locks=locks,
|
|
3264
|
+
replicas=replicas,
|
|
3265
|
+
source_replicas=source_replicas,
|
|
3266
|
+
rseselector=rseselector,
|
|
3267
|
+
rule=rule,
|
|
3268
|
+
preferred_rse_ids=preferred_rse_ids,
|
|
3269
|
+
source_rses=source_rses,
|
|
3270
|
+
session=session)
|
|
3271
|
+
|
|
3272
|
+
logger(logging.DEBUG, "Finished finding missing locks for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
|
|
3273
|
+
|
|
3274
|
+
|
|
3275
|
+
@transactional_session
|
|
3276
|
+
def __find_surplus_locks_and_remove_them(
|
|
3277
|
+
datasetfiles: 'Sequence[dict[str, Any]]',
|
|
3278
|
+
locks: dict[tuple[InternalScope, str], list[models.ReplicaLock]],
|
|
3279
|
+
rule: models.ReplicationRule,
|
|
3280
|
+
*,
|
|
3281
|
+
session: "Session",
|
|
3282
|
+
logger: LoggerFunction = logging.log
|
|
3283
|
+
) -> None:
|
|
3284
|
+
"""
|
|
3285
|
+
Find surplocks locks for a rule and delete them.
|
|
3286
|
+
|
|
3287
|
+
:param datasetfiles: Dict holding all datasets and files.
|
|
3288
|
+
:param locks: Dict holding locks.
|
|
3289
|
+
:param rule: The rule.
|
|
3290
|
+
:param session: Session of the db.
|
|
3291
|
+
:param logger: Optional decorated logger that can be passed from the calling daemons or servers.
|
|
3292
|
+
:raises: InsufficientAccountLimit, IntegrityError, InsufficientTargetRSEs
|
|
3293
|
+
:attention: This method modifies the contents of the locks and replicas input parameters.
|
|
3294
|
+
"""
|
|
3295
|
+
|
|
3296
|
+
logger(logging.DEBUG, "Finding surplus locks for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
|
|
3297
|
+
|
|
3298
|
+
account_counter_decreases = {} # {'rse_id': [file_size, file_size, file_size]}
|
|
3299
|
+
|
|
3300
|
+
# Put all the files in one dictionary
|
|
3301
|
+
files = {}
|
|
3302
|
+
for ds in datasetfiles:
|
|
3303
|
+
for file in ds['files']:
|
|
3304
|
+
files[(file['scope'], file['name'])] = True
|
|
3305
|
+
|
|
3306
|
+
for key in locks:
|
|
3307
|
+
if key not in files:
|
|
3308
|
+
# The lock needs to be removed
|
|
3309
|
+
for lock in deepcopy(locks[key]):
|
|
3310
|
+
if lock.rule_id == rule.id:
|
|
3311
|
+
__delete_lock_and_update_replica(lock=lock, purge_replicas=rule.purge_replicas, nowait=True, session=session)
|
|
3312
|
+
if lock.rse_id not in account_counter_decreases:
|
|
3313
|
+
account_counter_decreases[lock.rse_id] = []
|
|
3314
|
+
account_counter_decreases[lock.rse_id].append(lock.bytes)
|
|
3315
|
+
if lock.state == LockState.OK:
|
|
3316
|
+
rule.locks_ok_cnt -= 1
|
|
3317
|
+
elif lock.state == LockState.REPLICATING:
|
|
3318
|
+
rule.locks_replicating_cnt -= 1
|
|
3319
|
+
elif lock.state == LockState.STUCK:
|
|
3320
|
+
rule.locks_stuck_cnt -= 1
|
|
3321
|
+
locks[key].remove(lock)
|
|
3322
|
+
|
|
3323
|
+
logger(logging.DEBUG, "Finished finding surplus locks for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
|
|
3324
|
+
|
|
3325
|
+
|
|
3326
|
+
@transactional_session
|
|
3327
|
+
def __find_stuck_locks_and_repair_them(
|
|
3328
|
+
datasetfiles: 'Sequence[dict[str, Any]]',
|
|
3329
|
+
locks: dict[tuple[InternalScope, str], 'Sequence[models.ReplicaLock]'],
|
|
3330
|
+
replicas: dict[tuple[InternalScope, str], 'Sequence[models.CollectionReplica]'],
|
|
3331
|
+
source_replicas: dict[tuple[InternalScope, str], 'Sequence[models.CollectionReplica]'],
|
|
3332
|
+
rseselector: RSESelector,
|
|
3333
|
+
rule: models.ReplicationRule,
|
|
3334
|
+
source_rses: 'Sequence[str]',
|
|
3335
|
+
*,
|
|
3336
|
+
session: "Session",
|
|
3337
|
+
logger: LoggerFunction = logging.log
|
|
3338
|
+
) -> None:
|
|
3339
|
+
"""
|
|
3340
|
+
Find stuck locks for a rule and repair them.
|
|
3341
|
+
|
|
3342
|
+
:param datasetfiles: Dict holding all datasets and files.
|
|
3343
|
+
:param locks: Dict holding locks.
|
|
3344
|
+
:param replicas: Dict holding replicas.
|
|
3345
|
+
:param source_replicas: Dict holding source replicas.
|
|
3346
|
+
:param rseselector: The RSESelector to be used.
|
|
3347
|
+
:param rule: The rule.
|
|
3348
|
+
:param source_rses: RSE ids of eligible source RSEs.
|
|
3349
|
+
:param session: Session of the db.
|
|
3350
|
+
:param logger: Optional decorated logger that can be passed from the calling daemons or servers.
|
|
3351
|
+
:raises: InsufficientAccountLimit, IntegrityError, InsufficientTargetRSEs
|
|
3352
|
+
:attention: This method modifies the contents of the locks and replicas input parameters.
|
|
3353
|
+
"""
|
|
3354
|
+
|
|
3355
|
+
logger(logging.DEBUG, "Finding and repairing stuck locks for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
|
|
3356
|
+
|
|
3357
|
+
replicas_to_create, locks_to_create, transfers_to_create, \
|
|
3358
|
+
locks_to_delete = repair_stuck_locks_and_apply_rule_grouping(datasetfiles=datasetfiles,
|
|
3359
|
+
locks=locks,
|
|
3360
|
+
replicas=replicas,
|
|
3361
|
+
source_replicas=source_replicas,
|
|
3362
|
+
rseselector=rseselector,
|
|
3363
|
+
rule=rule,
|
|
3364
|
+
source_rses=source_rses,
|
|
3365
|
+
session=session)
|
|
3366
|
+
# Add the replicas
|
|
3367
|
+
session.add_all([item for sublist in replicas_to_create.values() for item in sublist])
|
|
3368
|
+
session.flush()
|
|
3369
|
+
|
|
3370
|
+
# Add the locks
|
|
3371
|
+
session.add_all([item for sublist in locks_to_create.values() for item in sublist])
|
|
3372
|
+
session.flush()
|
|
3373
|
+
|
|
3374
|
+
# Increase rse_counters
|
|
3375
|
+
for rse_id in replicas_to_create.keys():
|
|
3376
|
+
rse_counter.increase(rse_id=rse_id, files=len(replicas_to_create[rse_id]), bytes_=sum([replica.bytes for replica in replicas_to_create[rse_id]]), session=session)
|
|
3377
|
+
|
|
3378
|
+
# Increase account_counters
|
|
3379
|
+
for rse_id in locks_to_create.keys():
|
|
3380
|
+
account_counter.increase(rse_id=rse_id, account=rule.account, files=len(locks_to_create[rse_id]), bytes_=sum([lock.bytes for lock in locks_to_create[rse_id]]), session=session)
|
|
3381
|
+
|
|
3382
|
+
# Decrease account_counters
|
|
3383
|
+
for rse_id in locks_to_delete:
|
|
3384
|
+
account_counter.decrease(rse_id=rse_id, account=rule.account, files=len(locks_to_delete[rse_id]), bytes_=sum([lock.bytes for lock in locks_to_delete[rse_id]]), session=session)
|
|
3385
|
+
|
|
3386
|
+
# Delete the locks:
|
|
3387
|
+
for lock in [item for sublist in locks_to_delete.values() for item in sublist]:
|
|
3388
|
+
session.delete(lock)
|
|
3389
|
+
|
|
3390
|
+
# Add the transfers
|
|
3391
|
+
request_core.queue_requests(requests=transfers_to_create, session=session)
|
|
3392
|
+
session.flush()
|
|
3393
|
+
logger(logging.DEBUG, "Finished finding and repairing stuck locks for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
|
|
3394
|
+
|
|
3395
|
+
|
|
3396
|
+
@transactional_session
|
|
3397
|
+
def __evaluate_did_detach(
|
|
3398
|
+
eval_did: models.DataIdentifier,
|
|
3399
|
+
*,
|
|
3400
|
+
session: "Session",
|
|
3401
|
+
logger: LoggerFunction = logging.log
|
|
3402
|
+
) -> None:
|
|
3403
|
+
"""
|
|
3404
|
+
Evaluate a parent did which has children removed.
|
|
3405
|
+
|
|
3406
|
+
:param eval_did: The did object in use.
|
|
3407
|
+
:param session: The database session in use.
|
|
3408
|
+
:param logger: Optional decorated logger that can be passed from the calling daemons or servers.
|
|
3409
|
+
"""
|
|
3410
|
+
|
|
3411
|
+
logger(logging.INFO, "Re-Evaluating did %s:%s for DETACH", eval_did.scope, eval_did.name)
|
|
3412
|
+
force_epoch = config_get('rules', 'force_epoch_when_detach', default=False, session=session)
|
|
3413
|
+
|
|
3414
|
+
with METRICS.timer('evaluate_did_detach.total'):
|
|
3415
|
+
# Get all parent DID's
|
|
3416
|
+
parent_dids = rucio.core.did.list_all_parent_dids(scope=eval_did.scope, name=eval_did.name, session=session)
|
|
3417
|
+
|
|
3418
|
+
# Get all RR from parents and eval_did
|
|
3419
|
+
stmt = select(
|
|
3420
|
+
models.ReplicationRule
|
|
3421
|
+
).where(
|
|
3422
|
+
and_(models.ReplicationRule.scope == eval_did.scope,
|
|
3423
|
+
models.ReplicationRule.name == eval_did.name)
|
|
3424
|
+
).with_for_update(
|
|
3425
|
+
nowait=True
|
|
3426
|
+
)
|
|
3427
|
+
rules = list(session.execute(stmt).scalars().all())
|
|
3428
|
+
for did in parent_dids:
|
|
3429
|
+
stmt = select(
|
|
3430
|
+
models.ReplicationRule
|
|
3431
|
+
).where(
|
|
3432
|
+
and_(models.ReplicationRule.scope == did['scope'],
|
|
3433
|
+
models.ReplicationRule.name == did['name'])
|
|
3434
|
+
).with_for_update(
|
|
3435
|
+
nowait=True
|
|
3436
|
+
)
|
|
3437
|
+
rules.extend(session.execute(stmt).scalars().all())
|
|
3438
|
+
|
|
3439
|
+
# Iterate rules and delete locks
|
|
3440
|
+
transfers_to_delete = [] # [{'scope': , 'name':, 'rse_id':}]
|
|
3441
|
+
account_counter_decreases = {} # {'rse_id': [file_size, file_size, file_size]}
|
|
3442
|
+
for rule in rules:
|
|
3443
|
+
# Get all the files covering this rule
|
|
3444
|
+
files = {}
|
|
3445
|
+
for file in rucio.core.did.list_files(scope=rule.scope, name=rule.name, session=session):
|
|
3446
|
+
files[(file['scope'], file['name'])] = True
|
|
3447
|
+
logger(logging.DEBUG, "Removing locks for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
|
|
3448
|
+
rule_locks_ok_cnt_before = rule.locks_ok_cnt
|
|
3449
|
+
stmt = select(
|
|
3450
|
+
models.ReplicaLock
|
|
3451
|
+
).where(
|
|
3452
|
+
models.ReplicaLock.rule_id == rule.id
|
|
3453
|
+
)
|
|
3454
|
+
for lock in session.execute(stmt).scalars().all():
|
|
3455
|
+
if (lock.scope, lock.name) not in files:
|
|
3456
|
+
if __delete_lock_and_update_replica(lock=lock, purge_replicas=force_epoch or rule.purge_replicas, nowait=True, session=session):
|
|
3457
|
+
transfers_to_delete.append({'scope': lock.scope, 'name': lock.name, 'rse_id': lock.rse_id})
|
|
3458
|
+
if lock.rse_id not in account_counter_decreases:
|
|
3459
|
+
account_counter_decreases[lock.rse_id] = []
|
|
3460
|
+
account_counter_decreases[lock.rse_id].append(lock.bytes)
|
|
3461
|
+
if lock.state == LockState.OK:
|
|
3462
|
+
rule.locks_ok_cnt -= 1
|
|
3463
|
+
elif lock.state == LockState.REPLICATING:
|
|
3464
|
+
rule.locks_replicating_cnt -= 1
|
|
3465
|
+
elif lock.state == LockState.STUCK:
|
|
3466
|
+
rule.locks_stuck_cnt -= 1
|
|
3467
|
+
logger(logging.DEBUG, "Finished removing locks for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
|
|
3468
|
+
|
|
3469
|
+
if eval_did.did_type == DIDType.CONTAINER:
|
|
3470
|
+
# Get all datasets of eval_did
|
|
3471
|
+
child_datasets = {}
|
|
3472
|
+
for ds in rucio.core.did.list_child_datasets(scope=rule.scope, name=rule.name, session=session):
|
|
3473
|
+
child_datasets[(ds['scope'], ds['name'])] = True
|
|
3474
|
+
logger(logging.DEBUG, "Removing dataset_locks for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
|
|
3475
|
+
stmt = select(
|
|
3476
|
+
models.DatasetLock
|
|
3477
|
+
).where(
|
|
3478
|
+
models.DatasetLock.rule_id == rule.id
|
|
3479
|
+
)
|
|
3480
|
+
query = session.execute(stmt).scalars().all()
|
|
3481
|
+
for ds_lock in query:
|
|
3482
|
+
if (ds_lock.scope, ds_lock.name) not in child_datasets:
|
|
3483
|
+
ds_lock.delete(flush=False, session=session)
|
|
3484
|
+
logger(logging.DEBUG, "Finished removing dataset_locks for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
|
|
3485
|
+
|
|
3486
|
+
if rule.state == RuleState.SUSPENDED:
|
|
3487
|
+
pass
|
|
3488
|
+
elif rule.state == RuleState.STUCK:
|
|
3489
|
+
pass
|
|
3490
|
+
elif rule.locks_replicating_cnt == 0 and rule.locks_stuck_cnt == 0:
|
|
3491
|
+
rule.state = RuleState.OK
|
|
3492
|
+
if rule.grouping != RuleGrouping.NONE:
|
|
3493
|
+
stmt = update(
|
|
3494
|
+
models.DatasetLock
|
|
3495
|
+
).where(
|
|
3496
|
+
models.DatasetLock.rule_id == rule.id
|
|
3497
|
+
).values({
|
|
3498
|
+
models.DatasetLock.state: LockState.OK
|
|
3499
|
+
})
|
|
3500
|
+
session.execute(stmt)
|
|
3501
|
+
session.flush()
|
|
3502
|
+
if rule_locks_ok_cnt_before != rule.locks_ok_cnt:
|
|
3503
|
+
generate_rule_notifications(rule=rule, session=session)
|
|
3504
|
+
generate_email_for_rule_ok_notification(rule=rule, session=session)
|
|
3505
|
+
# Try to release potential parent rules
|
|
3506
|
+
release_parent_rule(child_rule_id=rule.id, session=session)
|
|
3507
|
+
|
|
3508
|
+
# Insert rule history
|
|
3509
|
+
insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
|
|
3510
|
+
|
|
3511
|
+
session.flush()
|
|
3512
|
+
|
|
3513
|
+
# Decrease account_counters
|
|
3514
|
+
for rse_id in account_counter_decreases:
|
|
3515
|
+
account_counter.decrease(rse_id=rse_id, account=rule.account, files=len(account_counter_decreases[rse_id]), bytes_=sum(account_counter_decreases[rse_id]), session=session)
|
|
3516
|
+
|
|
3517
|
+
for transfer in transfers_to_delete:
|
|
3518
|
+
transfers_to_cancel = request_core.cancel_request_did(scope=transfer['scope'], name=transfer['name'], dest_rse_id=transfer['rse_id'], session=session)
|
|
3519
|
+
transfer_core.cancel_transfers(transfers_to_cancel)
|
|
3520
|
+
|
|
3521
|
+
|
|
3522
|
+
@transactional_session
|
|
3523
|
+
def __oldest_file_under(
|
|
3524
|
+
scope: InternalScope,
|
|
3525
|
+
name: str,
|
|
3526
|
+
*,
|
|
3527
|
+
session: "Session"
|
|
3528
|
+
) -> Optional[tuple[InternalScope, str]]:
|
|
3529
|
+
"""
|
|
3530
|
+
Finds oldest file in oldest container/dataset in the container or the dataset, recursively.
|
|
3531
|
+
Oldest means attached to its parent first.
|
|
3532
|
+
|
|
3533
|
+
:param scope: dataset or container scope
|
|
3534
|
+
:param name: dataset or container name
|
|
3535
|
+
:returns: tuple (scope, name) or None
|
|
3536
|
+
"""
|
|
3537
|
+
stmt = select(
|
|
3538
|
+
models.DataIdentifierAssociation
|
|
3539
|
+
).where(
|
|
3540
|
+
and_(models.DataIdentifierAssociation.scope == scope,
|
|
3541
|
+
models.DataIdentifierAssociation.name == name)
|
|
3542
|
+
).order_by(
|
|
3543
|
+
models.DataIdentifierAssociation.created_at
|
|
3544
|
+
)
|
|
3545
|
+
children = session.execute(stmt).scalars().all()
|
|
3546
|
+
for child in children:
|
|
3547
|
+
if child.child_type == DIDType.FILE:
|
|
3548
|
+
return child.child_scope, child.child_name
|
|
3549
|
+
elif child.child_type in (DIDType.DATASET, DIDType.CONTAINER):
|
|
3550
|
+
out = __oldest_file_under(child.child_scope, child.child_name, session=session)
|
|
3551
|
+
if out:
|
|
3552
|
+
return out
|
|
3553
|
+
return None
|
|
3554
|
+
|
|
3555
|
+
|
|
3556
|
+
@transactional_session
|
|
3557
|
+
def __evaluate_did_attach(
|
|
3558
|
+
eval_did: models.DataIdentifier,
|
|
3559
|
+
*,
|
|
3560
|
+
session: "Session",
|
|
3561
|
+
logger: LoggerFunction = logging.log
|
|
3562
|
+
) -> None:
|
|
3563
|
+
"""
|
|
3564
|
+
Evaluate a parent did which has new children
|
|
3565
|
+
|
|
3566
|
+
:param eval_did: The did object in use.
|
|
3567
|
+
:param session: The database session in use.
|
|
3568
|
+
:param logger: Optional decorated logger that can be passed from the calling daemons or servers.
|
|
3569
|
+
:raises: ReplicationRuleCreationTemporaryFailed
|
|
3570
|
+
"""
|
|
3571
|
+
|
|
3572
|
+
logger(logging.INFO, "Re-Evaluating did %s:%s for ATTACH", eval_did.scope, eval_did.name)
|
|
3573
|
+
|
|
3574
|
+
with METRICS.timer('evaluate_did_attach.total'):
|
|
3575
|
+
# Get all parent DID's
|
|
3576
|
+
with METRICS.timer('evaluate_did_attach.list_parent_dids'):
|
|
3577
|
+
parent_dids = rucio.core.did.list_all_parent_dids(scope=eval_did.scope, name=eval_did.name, session=session)
|
|
3578
|
+
|
|
3579
|
+
# Get immediate new child DID's
|
|
3580
|
+
with METRICS.timer('evaluate_did_attach.list_new_child_dids'):
|
|
3581
|
+
stmt = select(
|
|
3582
|
+
models.DataIdentifierAssociation
|
|
3583
|
+
).with_hint(
|
|
3584
|
+
models.DataIdentifierAssociation, 'INDEX_RS_ASC(CONTENTS CONTENTS_PK)', 'oracle'
|
|
3585
|
+
).where(
|
|
3586
|
+
and_(models.DataIdentifierAssociation.scope == eval_did.scope,
|
|
3587
|
+
models.DataIdentifierAssociation.name == eval_did.name,
|
|
3588
|
+
models.DataIdentifierAssociation.rule_evaluation == true())
|
|
3589
|
+
)
|
|
3590
|
+
new_child_dids = session.execute(stmt).scalars().all()
|
|
3591
|
+
if new_child_dids:
|
|
3592
|
+
# Get all unsuspended RR from parents and eval_did
|
|
3593
|
+
with METRICS.timer('evaluate_did_attach.get_rules'):
|
|
3594
|
+
rule_clauses = []
|
|
3595
|
+
for did in parent_dids:
|
|
3596
|
+
rule_clauses.append(and_(models.ReplicationRule.scope == did['scope'],
|
|
3597
|
+
models.ReplicationRule.name == did['name']))
|
|
3598
|
+
rule_clauses.append(and_(models.ReplicationRule.scope == eval_did.scope,
|
|
3599
|
+
models.ReplicationRule.name == eval_did.name))
|
|
3600
|
+
stmt = select(
|
|
3601
|
+
models.ReplicationRule
|
|
3602
|
+
).where(
|
|
3603
|
+
and_(or_(*rule_clauses),
|
|
3604
|
+
models.ReplicationRule.state.not_in([RuleState.SUSPENDED,
|
|
3605
|
+
RuleState.WAITING_APPROVAL,
|
|
3606
|
+
RuleState.INJECT]))
|
|
3607
|
+
).with_for_update(
|
|
3608
|
+
nowait=True
|
|
3609
|
+
)
|
|
3610
|
+
rules = session.execute(stmt).scalars().all()
|
|
3611
|
+
if rules:
|
|
3612
|
+
# Resolve the new_child_dids to its locks
|
|
3613
|
+
with METRICS.timer('evaluate_did_attach.resolve_did_to_locks_and_replicas'):
|
|
3614
|
+
# Resolve the rules to possible target rses:
|
|
3615
|
+
possible_rses = []
|
|
3616
|
+
source_rses = []
|
|
3617
|
+
for rule in rules:
|
|
3618
|
+
try:
|
|
3619
|
+
vo = rule.account.vo
|
|
3620
|
+
if rule.source_replica_expression:
|
|
3621
|
+
source_rses.extend(parse_expression(rule.source_replica_expression, filter_={'vo': vo}, session=session))
|
|
3622
|
+
|
|
3623
|
+
# if rule.ignore_availability:
|
|
3624
|
+
possible_rses.extend(parse_expression(rule.rse_expression, filter_={'vo': vo}, session=session))
|
|
3625
|
+
# else:
|
|
3626
|
+
# possible_rses.extend(parse_expression(rule.rse_expression, filter={'availability_write': True}, session=session))
|
|
3627
|
+
except (InvalidRSEExpression, RSEWriteBlocked):
|
|
3628
|
+
possible_rses = []
|
|
3629
|
+
break
|
|
3630
|
+
|
|
3631
|
+
source_rses = list(set([rse['id'] for rse in source_rses]))
|
|
3632
|
+
possible_rses = list(set([rse['id'] for rse in possible_rses]))
|
|
3633
|
+
|
|
3634
|
+
datasetfiles, locks, replicas, source_replicas = __resolve_dids_to_locks_and_replicas(dids=new_child_dids,
|
|
3635
|
+
nowait=True,
|
|
3636
|
+
restrict_rses=possible_rses,
|
|
3637
|
+
source_rses=source_rses,
|
|
3638
|
+
session=session)
|
|
3639
|
+
|
|
3640
|
+
# Evaluate the replication rules
|
|
3641
|
+
with METRICS.timer('evaluate_did_attach.evaluate_rules'):
|
|
3642
|
+
for rule in rules:
|
|
3643
|
+
rule_locks_ok_cnt_before = rule.locks_ok_cnt
|
|
3644
|
+
|
|
3645
|
+
# 1. Resolve the rse_expression into a list of RSE-ids
|
|
3646
|
+
try:
|
|
3647
|
+
vo = rule.account.vo
|
|
3648
|
+
if rule.ignore_availability:
|
|
3649
|
+
rses = parse_expression(rule.rse_expression, filter_={'vo': vo}, session=session)
|
|
3650
|
+
else:
|
|
3651
|
+
rses = parse_expression(rule.rse_expression, filter_={'vo': vo, 'availability_write': True}, session=session)
|
|
3652
|
+
source_rses = []
|
|
3653
|
+
if rule.source_replica_expression:
|
|
3654
|
+
source_rses = parse_expression(rule.source_replica_expression, filter_={'vo': vo}, session=session)
|
|
3655
|
+
except (InvalidRSEExpression, RSEWriteBlocked) as error:
|
|
3656
|
+
rule.state = RuleState.STUCK
|
|
3657
|
+
rule.error = (str(error)[:245] + '...') if len(str(error)) > 245 else str(error)
|
|
3658
|
+
rule.save(session=session)
|
|
3659
|
+
# Insert rule history
|
|
3660
|
+
insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
|
|
3661
|
+
# Try to update the DatasetLocks
|
|
3662
|
+
if rule.grouping != RuleGrouping.NONE:
|
|
3663
|
+
stmt = update(
|
|
3664
|
+
models.DatasetLock
|
|
3665
|
+
).where(
|
|
3666
|
+
models.DatasetLock.rule_id == rule.id
|
|
3667
|
+
).values({
|
|
3668
|
+
models.DatasetLock.state: LockState.STUCK
|
|
3669
|
+
})
|
|
3670
|
+
session.execute(stmt)
|
|
3671
|
+
continue
|
|
3672
|
+
|
|
3673
|
+
# 2. Create the RSE Selector
|
|
3674
|
+
try:
|
|
3675
|
+
rseselector = RSESelector(account=rule.account,
|
|
3676
|
+
rses=rses,
|
|
3677
|
+
weight=rule.weight,
|
|
3678
|
+
copies=rule.copies,
|
|
3679
|
+
ignore_account_limit=rule.ignore_account_limit,
|
|
3680
|
+
session=session)
|
|
3681
|
+
except (InvalidRuleWeight, InsufficientTargetRSEs, InsufficientAccountLimit, RSEOverQuota) as error:
|
|
3682
|
+
rule.state = RuleState.STUCK
|
|
3683
|
+
rule.error = (str(error)[:245] + '...') if len(str(error)) > 245 else str(error)
|
|
3684
|
+
rule.save(session=session)
|
|
3685
|
+
# Insert rule history
|
|
3686
|
+
insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
|
|
3687
|
+
# Try to update the DatasetLocks
|
|
3688
|
+
if rule.grouping != RuleGrouping.NONE:
|
|
3689
|
+
stmt = update(
|
|
3690
|
+
models.DatasetLock
|
|
3691
|
+
).where(
|
|
3692
|
+
models.DatasetLock.rule_id == rule.id
|
|
3693
|
+
).values({
|
|
3694
|
+
models.DatasetLock.state: LockState.STUCK
|
|
3695
|
+
})
|
|
3696
|
+
session.execute(stmt)
|
|
3697
|
+
continue
|
|
3698
|
+
|
|
3699
|
+
# 3. Apply the Replication rule to the Files
|
|
3700
|
+
preferred_rse_ids = []
|
|
3701
|
+
brother_scope_name = None
|
|
3702
|
+
|
|
3703
|
+
if rule.grouping == RuleGrouping.ALL:
|
|
3704
|
+
# get oldest file, recursively, in the rule owner
|
|
3705
|
+
brother_scope_name = __oldest_file_under(rule["scope"], rule["name"], session=session)
|
|
3706
|
+
|
|
3707
|
+
elif rule.grouping == RuleGrouping.DATASET and new_child_dids[0].child_type == DIDType.FILE:
|
|
3708
|
+
# get oldest file in the dataset being evaluated
|
|
3709
|
+
brother_scope_name = __oldest_file_under(eval_did.scope, eval_did.name, session=session)
|
|
3710
|
+
|
|
3711
|
+
if brother_scope_name:
|
|
3712
|
+
scope, name = brother_scope_name
|
|
3713
|
+
file_locks = rucio.core.lock.get_replica_locks(scope=scope, name=name, nowait=True, session=session)
|
|
3714
|
+
preferred_rse_ids = [
|
|
3715
|
+
lock['rse_id']
|
|
3716
|
+
for lock in file_locks
|
|
3717
|
+
if lock['rse_id'] in [rse['id'] for rse in rses] and lock['rule_id'] == rule.id
|
|
3718
|
+
]
|
|
3719
|
+
|
|
3720
|
+
locks_stuck_before = rule.locks_stuck_cnt
|
|
3721
|
+
try:
|
|
3722
|
+
__create_locks_replicas_transfers(datasetfiles=datasetfiles,
|
|
3723
|
+
locks=locks,
|
|
3724
|
+
replicas=replicas,
|
|
3725
|
+
source_replicas=source_replicas,
|
|
3726
|
+
rseselector=rseselector,
|
|
3727
|
+
rule=rule,
|
|
3728
|
+
preferred_rse_ids=preferred_rse_ids,
|
|
3729
|
+
source_rses=[rse['id'] for rse in source_rses],
|
|
3730
|
+
session=session)
|
|
3731
|
+
except (InsufficientAccountLimit, InsufficientTargetRSEs, RSEOverQuota) as error:
|
|
3732
|
+
rule.state = RuleState.STUCK
|
|
3733
|
+
rule.error = (str(error)[:245] + '...') if len(str(error)) > 245 else str(error)
|
|
3734
|
+
rule.save(session=session)
|
|
3735
|
+
# Insert rule history
|
|
3736
|
+
insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
|
|
3737
|
+
# Try to update the DatasetLocks
|
|
3738
|
+
if rule.grouping != RuleGrouping.NONE:
|
|
3739
|
+
stmt = update(
|
|
3740
|
+
models.DatasetLock
|
|
3741
|
+
).where(
|
|
3742
|
+
models.DatasetLock.rule_id == rule.id
|
|
3743
|
+
).values({
|
|
3744
|
+
models.DatasetLock.state: LockState.STUCK
|
|
3745
|
+
})
|
|
3746
|
+
session.execute(stmt)
|
|
3747
|
+
continue
|
|
3748
|
+
|
|
3749
|
+
# 4. Update the Rule State
|
|
3750
|
+
if rule.state == RuleState.STUCK:
|
|
3751
|
+
pass
|
|
3752
|
+
elif rule.locks_stuck_cnt > 0:
|
|
3753
|
+
if locks_stuck_before != rule.locks_stuck_cnt:
|
|
3754
|
+
rule.state = RuleState.STUCK
|
|
3755
|
+
rule.error = 'MissingSourceReplica'
|
|
3756
|
+
stmt = update(
|
|
3757
|
+
models.DatasetLock
|
|
3758
|
+
).where(
|
|
3759
|
+
models.DatasetLock.rule_id == rule.id
|
|
3760
|
+
).values({
|
|
3761
|
+
models.DatasetLock.state: LockState.STUCK
|
|
3762
|
+
})
|
|
3763
|
+
session.execute(stmt)
|
|
3764
|
+
elif rule.locks_replicating_cnt > 0:
|
|
3765
|
+
rule.state = RuleState.REPLICATING
|
|
3766
|
+
if rule.grouping != RuleGrouping.NONE:
|
|
3767
|
+
stmt = update(
|
|
3768
|
+
models.DatasetLock
|
|
3769
|
+
).where(
|
|
3770
|
+
models.DatasetLock.rule_id == rule.id
|
|
3771
|
+
).values({
|
|
3772
|
+
models.DatasetLock.state: LockState.REPLICATING
|
|
3773
|
+
})
|
|
3774
|
+
session.execute(stmt)
|
|
3775
|
+
elif rule.locks_replicating_cnt == 0 and rule.locks_stuck_cnt == 0:
|
|
3776
|
+
rule.state = RuleState.OK
|
|
3777
|
+
if rule.grouping != RuleGrouping.NONE:
|
|
3778
|
+
stmt = update(
|
|
3779
|
+
models.DatasetLock
|
|
3780
|
+
).where(
|
|
3781
|
+
models.DatasetLock.rule_id == rule.id
|
|
3782
|
+
).values({
|
|
3783
|
+
models.DatasetLock.state: LockState.OK
|
|
3784
|
+
})
|
|
3785
|
+
session.execute(stmt)
|
|
3786
|
+
session.flush()
|
|
3787
|
+
if rule_locks_ok_cnt_before < rule.locks_ok_cnt:
|
|
3788
|
+
generate_rule_notifications(rule=rule, session=session)
|
|
3789
|
+
generate_email_for_rule_ok_notification(rule=rule, session=session)
|
|
3790
|
+
|
|
3791
|
+
# Insert rule history
|
|
3792
|
+
insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
|
|
3793
|
+
|
|
3794
|
+
# Unflage the dids
|
|
3795
|
+
with METRICS.timer('evaluate_did_attach.update_did'):
|
|
3796
|
+
for did in new_child_dids:
|
|
3797
|
+
did.rule_evaluation = None
|
|
3798
|
+
|
|
3799
|
+
session.flush()
|
|
3800
|
+
|
|
3801
|
+
|
|
3802
|
+
@transactional_session
|
|
3803
|
+
def __resolve_did_to_locks_and_replicas(
|
|
3804
|
+
did: models.DataIdentifier,
|
|
3805
|
+
nowait: bool = False,
|
|
3806
|
+
restrict_rses: Optional['Sequence[str]'] = None,
|
|
3807
|
+
source_rses: Optional['Sequence[str]'] = None,
|
|
3808
|
+
only_stuck: bool = False,
|
|
3809
|
+
*,
|
|
3810
|
+
session: "Session"
|
|
3811
|
+
) -> tuple[list[dict[str, Any]],
|
|
3812
|
+
dict[tuple[str, str], models.ReplicaLock],
|
|
3813
|
+
dict[tuple[str, str], models.RSEFileAssociation],
|
|
3814
|
+
dict[tuple[str, str], str]]:
|
|
3815
|
+
"""
|
|
3816
|
+
Resolves a did to its constituent children and reads the locks and replicas of all the constituent files.
|
|
3817
|
+
|
|
3818
|
+
:param did: The db object of the did the rule is applied on.
|
|
3819
|
+
:param nowait: Nowait parameter for the FOR UPDATE statement.
|
|
3820
|
+
:param restrict_rses: Possible rses of the rule, so only these replica/locks should be considered.
|
|
3821
|
+
:param source_rses: Source rses for this rule. These replicas are not row-locked.
|
|
3822
|
+
:param only_stuck: Get results only for STUCK locks, if True.
|
|
3823
|
+
:param session: Session of the db.
|
|
3824
|
+
:returns: (datasetfiles, locks, replicas, source_replicas)
|
|
3825
|
+
"""
|
|
3826
|
+
|
|
3827
|
+
datasetfiles = [] # List of Datasets and their files in the Tree [{'scope':, 'name':, 'files': []}]
|
|
3828
|
+
# Files are in the format [{'scope':, 'name':, 'bytes':, 'md5':, 'adler32':}]
|
|
3829
|
+
locks = {} # {(scope,name): [SQLAlchemy]}
|
|
3830
|
+
replicas = {} # {(scope, name): [SQLAlchemy]}
|
|
3831
|
+
source_replicas = {} # {(scope, name): [rse_id]
|
|
3832
|
+
|
|
3833
|
+
if did.did_type == DIDType.FILE:
|
|
3834
|
+
datasetfiles = [{'scope': None,
|
|
3835
|
+
'name': None,
|
|
3836
|
+
'files': [{'scope': did.scope,
|
|
3837
|
+
'name': did.name,
|
|
3838
|
+
'bytes': did.bytes,
|
|
3839
|
+
'md5': did.md5,
|
|
3840
|
+
'adler32': did.adler32}]}]
|
|
3841
|
+
locks[(did.scope, did.name)] = rucio.core.lock.get_replica_locks(scope=did.scope, name=did.name, nowait=nowait, restrict_rses=restrict_rses, session=session)
|
|
3842
|
+
replicas[(did.scope, did.name)] = rucio.core.replica.get_and_lock_file_replicas(scope=did.scope, name=did.name, nowait=nowait, restrict_rses=restrict_rses, session=session)
|
|
3843
|
+
if source_rses:
|
|
3844
|
+
source_replicas[(did.scope, did.name)] = rucio.core.replica.get_source_replicas(scope=did.scope, name=did.name, source_rses=source_rses, session=session)
|
|
3845
|
+
|
|
3846
|
+
elif did.did_type == DIDType.DATASET and only_stuck:
|
|
3847
|
+
files = []
|
|
3848
|
+
locks = rucio.core.lock.get_files_and_replica_locks_of_dataset(scope=did.scope, name=did.name, nowait=nowait, restrict_rses=restrict_rses, only_stuck=True, session=session)
|
|
3849
|
+
for file in locks:
|
|
3850
|
+
file_did = rucio.core.did.get_did(scope=file[0], name=file[1], session=session)
|
|
3851
|
+
files.append({'scope': file[0], 'name': file[1], 'bytes': file_did['bytes'], 'md5': file_did['md5'], 'adler32': file_did['adler32']})
|
|
3852
|
+
replicas[(file[0], file[1])] = rucio.core.replica.get_and_lock_file_replicas(scope=file[0], name=file[1], nowait=nowait, restrict_rses=restrict_rses, session=session)
|
|
3853
|
+
if source_rses:
|
|
3854
|
+
source_replicas[(file[0], file[1])] = rucio.core.replica.get_source_replicas(scope=file[0], name=file[1], source_rses=source_rses, session=session)
|
|
3855
|
+
datasetfiles = [{'scope': did.scope,
|
|
3856
|
+
'name': did.name,
|
|
3857
|
+
'files': files}]
|
|
3858
|
+
|
|
3859
|
+
elif did.did_type == DIDType.DATASET:
|
|
3860
|
+
files, replicas = rucio.core.replica.get_and_lock_file_replicas_for_dataset(scope=did.scope, name=did.name, nowait=nowait, restrict_rses=restrict_rses, session=session)
|
|
3861
|
+
if source_rses:
|
|
3862
|
+
source_replicas = rucio.core.replica.get_source_replicas_for_dataset(scope=did.scope, name=did.name, source_rses=source_rses, session=session)
|
|
3863
|
+
datasetfiles = [{'scope': did.scope,
|
|
3864
|
+
'name': did.name,
|
|
3865
|
+
'files': files}]
|
|
3866
|
+
locks = rucio.core.lock.get_files_and_replica_locks_of_dataset(scope=did.scope, name=did.name, nowait=nowait, restrict_rses=restrict_rses, session=session)
|
|
3867
|
+
|
|
3868
|
+
elif did.did_type == DIDType.CONTAINER and only_stuck:
|
|
3869
|
+
|
|
3870
|
+
for dataset in rucio.core.did.list_child_datasets(scope=did.scope, name=did.name, session=session):
|
|
3871
|
+
files = []
|
|
3872
|
+
tmp_locks = rucio.core.lock.get_files_and_replica_locks_of_dataset(scope=dataset['scope'], name=dataset['name'], nowait=nowait, restrict_rses=restrict_rses, only_stuck=True, session=session)
|
|
3873
|
+
locks = dict(list(locks.items()) + list(tmp_locks.items()))
|
|
3874
|
+
for file in tmp_locks:
|
|
3875
|
+
file_did = rucio.core.did.get_did(scope=file[0], name=file[1], session=session)
|
|
3876
|
+
files.append({'scope': file[0], 'name': file[1], 'bytes': file_did['bytes'], 'md5': file_did['md5'], 'adler32': file_did['adler32']})
|
|
3877
|
+
replicas[(file[0], file[1])] = rucio.core.replica.get_and_lock_file_replicas(scope=file[0], name=file[1], nowait=nowait, restrict_rses=restrict_rses, session=session)
|
|
3878
|
+
if source_rses:
|
|
3879
|
+
source_replicas[(file[0], file[1])] = rucio.core.replica.get_source_replicas(scope=file[0], name=file[1], source_rses=source_rses, session=session)
|
|
3880
|
+
datasetfiles.append({'scope': dataset['scope'],
|
|
3881
|
+
'name': dataset['name'],
|
|
3882
|
+
'files': files})
|
|
3883
|
+
|
|
3884
|
+
elif did.did_type == DIDType.CONTAINER:
|
|
3885
|
+
|
|
3886
|
+
for dataset in rucio.core.did.list_child_datasets(scope=did.scope, name=did.name, session=session):
|
|
3887
|
+
files, tmp_replicas = rucio.core.replica.get_and_lock_file_replicas_for_dataset(scope=dataset['scope'], name=dataset['name'], nowait=nowait, restrict_rses=restrict_rses, session=session)
|
|
3888
|
+
if source_rses:
|
|
3889
|
+
tmp_source_replicas = rucio.core.replica.get_source_replicas_for_dataset(scope=dataset['scope'], name=dataset['name'], source_rses=source_rses, session=session)
|
|
3890
|
+
source_replicas = dict(list(source_replicas.items()) + list(tmp_source_replicas.items()))
|
|
3891
|
+
tmp_locks = rucio.core.lock.get_files_and_replica_locks_of_dataset(scope=dataset['scope'], name=dataset['name'], nowait=nowait, restrict_rses=restrict_rses, session=session)
|
|
3892
|
+
datasetfiles.append({'scope': dataset['scope'],
|
|
3893
|
+
'name': dataset['name'],
|
|
3894
|
+
'files': files})
|
|
3895
|
+
replicas = dict(list(replicas.items()) + list(tmp_replicas.items()))
|
|
3896
|
+
locks = dict(list(locks.items()) + list(tmp_locks.items()))
|
|
3897
|
+
|
|
3898
|
+
# order datasetfiles for deterministic result
|
|
3899
|
+
try:
|
|
3900
|
+
datasetfiles = sorted(datasetfiles, key=lambda x: "%s%s" % (x['scope'], x['name']))
|
|
3901
|
+
except Exception:
|
|
3902
|
+
pass
|
|
3903
|
+
|
|
3904
|
+
else:
|
|
3905
|
+
raise InvalidReplicationRule('The did \"%s:%s\" has been deleted.' % (did.scope, did.name))
|
|
3906
|
+
|
|
3907
|
+
return datasetfiles, locks, replicas, source_replicas
|
|
3908
|
+
|
|
3909
|
+
|
|
3910
|
+
@transactional_session
|
|
3911
|
+
def __resolve_dids_to_locks_and_replicas(
|
|
3912
|
+
dids: 'Sequence[models.DataIdentifierAssociation]',
|
|
3913
|
+
nowait: bool = False,
|
|
3914
|
+
restrict_rses: Optional['Sequence[str]'] = None,
|
|
3915
|
+
source_rses: Optional['Sequence[str]'] = None,
|
|
3916
|
+
*,
|
|
3917
|
+
session: "Session"
|
|
3918
|
+
) -> tuple[list[dict[str, Any]],
|
|
3919
|
+
dict[tuple[str, str], models.ReplicaLock],
|
|
3920
|
+
dict[tuple[str, str], models.RSEFileAssociation],
|
|
3921
|
+
dict[tuple[str, str], str]]:
|
|
3922
|
+
"""
|
|
3923
|
+
Resolves a list of dids to its constituent children and reads the locks and replicas of all the constituent files.
|
|
3924
|
+
|
|
3925
|
+
:param dids: The list of DataIdentifierAssociation objects.
|
|
3926
|
+
:param nowait: Nowait parameter for the FOR UPDATE statement.
|
|
3927
|
+
:param restrict_rses: Possible rses of the rule, so only these replica/locks should be considered.
|
|
3928
|
+
:param source_rses: Source rses for this rule. These replicas are not row-locked.
|
|
3929
|
+
:param session: Session of the db.
|
|
3930
|
+
:returns: (datasetfiles, locks, replicas, source_replicas)
|
|
3931
|
+
"""
|
|
3932
|
+
|
|
3933
|
+
datasetfiles = [] # List of Datasets and their files in the Tree [{'scope':, 'name':, 'files': []}]
|
|
3934
|
+
# Files are in the format [{'scope':, 'name':, 'bytes':, 'md5':, 'adler32':}]
|
|
3935
|
+
locks = {} # {(scope,name): [SQLAlchemy]}
|
|
3936
|
+
replicas = {} # {(scope, name): [SQLAlchemy]}
|
|
3937
|
+
source_replicas = {} # {(scope, name): [rse_id]
|
|
3938
|
+
restrict_rses = restrict_rses or []
|
|
3939
|
+
|
|
3940
|
+
if dids[0].child_type == DIDType.FILE:
|
|
3941
|
+
# All the dids will be files!
|
|
3942
|
+
# Prepare the datasetfiles
|
|
3943
|
+
files = []
|
|
3944
|
+
for did in dids:
|
|
3945
|
+
files.append({'scope': did.child_scope,
|
|
3946
|
+
'name': did.child_name,
|
|
3947
|
+
'bytes': did.bytes,
|
|
3948
|
+
'md5': did.md5,
|
|
3949
|
+
'adler32': did.adler32})
|
|
3950
|
+
locks[(did.child_scope, did.child_name)] = []
|
|
3951
|
+
replicas[(did.child_scope, did.child_name)] = []
|
|
3952
|
+
source_replicas[(did.child_scope, did.child_name)] = []
|
|
3953
|
+
datasetfiles = [{'scope': dids[0].scope, 'name': dids[0].name, 'files': files}]
|
|
3954
|
+
|
|
3955
|
+
# Prepare the locks and files
|
|
3956
|
+
lock_clauses = []
|
|
3957
|
+
replica_clauses = []
|
|
3958
|
+
for did in dids:
|
|
3959
|
+
lock_clauses.append(and_(models.ReplicaLock.scope == did.child_scope,
|
|
3960
|
+
models.ReplicaLock.name == did.child_name))
|
|
3961
|
+
replica_clauses.append(and_(models.RSEFileAssociation.scope == did.child_scope,
|
|
3962
|
+
models.RSEFileAssociation.name == did.child_name))
|
|
3963
|
+
lock_clause_chunks = [lock_clauses[x:x + 10] for x in range(0, len(lock_clauses), 10)]
|
|
3964
|
+
replica_clause_chunks = [replica_clauses[x:x + 10] for x in range(0, len(replica_clauses), 10)]
|
|
3965
|
+
|
|
3966
|
+
replicas_rse_clause = []
|
|
3967
|
+
source_replicas_rse_clause = []
|
|
3968
|
+
locks_rse_clause = []
|
|
3969
|
+
if restrict_rses:
|
|
3970
|
+
for rse_id in restrict_rses:
|
|
3971
|
+
replicas_rse_clause.append(models.RSEFileAssociation.rse_id == rse_id)
|
|
3972
|
+
locks_rse_clause.append(models.ReplicaLock.rse_id == rse_id)
|
|
3973
|
+
if source_rses:
|
|
3974
|
+
for rse_id in source_rses:
|
|
3975
|
+
source_replicas_rse_clause.append(models.RSEFileAssociation.rse_id == rse_id)
|
|
3976
|
+
|
|
3977
|
+
for lock_clause_chunk in lock_clause_chunks:
|
|
3978
|
+
if locks_rse_clause:
|
|
3979
|
+
stmt = select(
|
|
3980
|
+
models.ReplicaLock
|
|
3981
|
+
).with_hint(
|
|
3982
|
+
models.ReplicaLock, 'INDEX(LOCKS LOCKS_PK)', 'oracle'
|
|
3983
|
+
).where(
|
|
3984
|
+
and_(or_(*lock_clause_chunk),
|
|
3985
|
+
or_(*locks_rse_clause))
|
|
3986
|
+
).with_for_update(
|
|
3987
|
+
nowait=nowait
|
|
3988
|
+
)
|
|
3989
|
+
tmp_locks = session.execute(stmt).scalars().all()
|
|
3990
|
+
else:
|
|
3991
|
+
stmt = select(
|
|
3992
|
+
models.ReplicaLock
|
|
3993
|
+
).with_hint(
|
|
3994
|
+
models.ReplicaLock, 'INDEX(LOCKS LOCKS_PK)', 'oracle'
|
|
3995
|
+
).where(
|
|
3996
|
+
or_(*lock_clause_chunk)
|
|
3997
|
+
).with_for_update(
|
|
3998
|
+
nowait=nowait
|
|
3999
|
+
)
|
|
4000
|
+
tmp_locks = session.execute(stmt).scalars().all()
|
|
4001
|
+
for lock in tmp_locks:
|
|
4002
|
+
if (lock.scope, lock.name) not in locks:
|
|
4003
|
+
locks[(lock.scope, lock.name)] = [lock]
|
|
4004
|
+
else:
|
|
4005
|
+
locks[(lock.scope, lock.name)].append(lock)
|
|
4006
|
+
|
|
4007
|
+
for replica_clause_chunk in replica_clause_chunks:
|
|
4008
|
+
if replicas_rse_clause:
|
|
4009
|
+
stmt = select(
|
|
4010
|
+
models.RSEFileAssociation
|
|
4011
|
+
).with_hint(
|
|
4012
|
+
models.RSEFileAssociation, 'INDEX(REPLICAS REPLICAS_PK)', 'oracle'
|
|
4013
|
+
).where(
|
|
4014
|
+
and_(or_(*replica_clause_chunk),
|
|
4015
|
+
or_(*replicas_rse_clause),
|
|
4016
|
+
models.RSEFileAssociation.state != ReplicaState.BEING_DELETED)
|
|
4017
|
+
).with_for_update(
|
|
4018
|
+
nowait=nowait
|
|
4019
|
+
)
|
|
4020
|
+
tmp_replicas = session.execute(stmt).scalars().all()
|
|
4021
|
+
else:
|
|
4022
|
+
stmt = select(
|
|
4023
|
+
models.RSEFileAssociation
|
|
4024
|
+
).with_hint(
|
|
4025
|
+
models.RSEFileAssociation, 'INDEX(REPLICAS REPLICAS_PK)', 'oracle'
|
|
4026
|
+
).where(
|
|
4027
|
+
and_(or_(*replica_clause_chunk),
|
|
4028
|
+
models.RSEFileAssociation.state != ReplicaState.BEING_DELETED)
|
|
4029
|
+
).with_for_update(
|
|
4030
|
+
nowait=nowait
|
|
4031
|
+
)
|
|
4032
|
+
tmp_replicas = session.execute(stmt).scalars().all()
|
|
4033
|
+
for replica in tmp_replicas:
|
|
4034
|
+
if (replica.scope, replica.name) not in replicas:
|
|
4035
|
+
replicas[(replica.scope, replica.name)] = [replica]
|
|
4036
|
+
else:
|
|
4037
|
+
replicas[(replica.scope, replica.name)].append(replica)
|
|
4038
|
+
|
|
4039
|
+
if source_rses:
|
|
4040
|
+
for replica_clause_chunk in replica_clause_chunks:
|
|
4041
|
+
stmt = select(
|
|
4042
|
+
models.RSEFileAssociation.scope,
|
|
4043
|
+
models.RSEFileAssociation.name,
|
|
4044
|
+
models.RSEFileAssociation.rse_id
|
|
4045
|
+
).with_hint(
|
|
4046
|
+
models.RSEFileAssociation, 'INDEX(REPLICAS REPLICAS_PK)', 'oracle'
|
|
4047
|
+
).where(
|
|
4048
|
+
and_(or_(*replica_clause_chunk),
|
|
4049
|
+
or_(*source_replicas_rse_clause),
|
|
4050
|
+
models.RSEFileAssociation.state == ReplicaState.AVAILABLE)
|
|
4051
|
+
)
|
|
4052
|
+
tmp_source_replicas = session.execute(stmt).all()
|
|
4053
|
+
for scope, name, rse_id in tmp_source_replicas:
|
|
4054
|
+
if (scope, name) not in source_replicas:
|
|
4055
|
+
source_replicas[(scope, name)] = [rse_id]
|
|
4056
|
+
else:
|
|
4057
|
+
source_replicas[(scope, name)].append(rse_id)
|
|
4058
|
+
else:
|
|
4059
|
+
# The evaluate_dids will be containers and/or datasets
|
|
4060
|
+
for did in dids:
|
|
4061
|
+
stmt = select(
|
|
4062
|
+
models.DataIdentifier
|
|
4063
|
+
).where(
|
|
4064
|
+
and_(models.DataIdentifier.scope == did.child_scope,
|
|
4065
|
+
models.DataIdentifier.name == did.child_name)
|
|
4066
|
+
)
|
|
4067
|
+
real_did = session.execute(stmt).scalar_one()
|
|
4068
|
+
tmp_datasetfiles, tmp_locks, tmp_replicas, tmp_source_replicas = __resolve_did_to_locks_and_replicas(did=real_did,
|
|
4069
|
+
nowait=nowait,
|
|
4070
|
+
restrict_rses=restrict_rses,
|
|
4071
|
+
source_rses=source_rses,
|
|
4072
|
+
session=session)
|
|
4073
|
+
datasetfiles.extend(tmp_datasetfiles)
|
|
4074
|
+
locks.update(tmp_locks)
|
|
4075
|
+
replicas.update(tmp_replicas)
|
|
4076
|
+
source_replicas.update(tmp_source_replicas)
|
|
4077
|
+
return datasetfiles, locks, replicas, source_replicas
|
|
4078
|
+
|
|
4079
|
+
|
|
4080
|
+
@transactional_session
|
|
4081
|
+
def __create_locks_replicas_transfers(
|
|
4082
|
+
datasetfiles: 'Sequence[dict[str, Any]]',
|
|
4083
|
+
locks: dict[tuple[InternalScope, str], 'Sequence[models.ReplicaLock]'],
|
|
4084
|
+
replicas: dict[tuple[InternalScope, str], 'Sequence[models.CollectionReplica]'],
|
|
4085
|
+
source_replicas: dict[tuple[InternalScope, str], 'Sequence[models.CollectionReplica]'],
|
|
4086
|
+
rseselector: RSESelector,
|
|
4087
|
+
rule: models.ReplicationRule,
|
|
4088
|
+
preferred_rse_ids: Optional['Sequence[str]'] = None,
|
|
4089
|
+
source_rses: Optional['Sequence[str]'] = None,
|
|
4090
|
+
*,
|
|
4091
|
+
session: "Session",
|
|
4092
|
+
logger: LoggerFunction = logging.log
|
|
4093
|
+
) -> None:
|
|
4094
|
+
"""
|
|
4095
|
+
Apply a created replication rule to a set of files
|
|
4096
|
+
|
|
4097
|
+
:param datasetfiles: Dict holding all datasets and files.
|
|
4098
|
+
:param locks: Dict holding locks.
|
|
4099
|
+
:param replicas: Dict holding replicas.
|
|
4100
|
+
:param source_replicas: Dict holding source replicas.
|
|
4101
|
+
:param rseselector: The RSESelector to be used.
|
|
4102
|
+
:param rule: The rule.
|
|
4103
|
+
:param preferred_rse_ids: Preferred RSE's to select.
|
|
4104
|
+
:param source_rses: RSE ids of eligible source replicas.
|
|
4105
|
+
:param session: Session of the db.
|
|
4106
|
+
:param logger: Optional decorated logger that can be passed from the calling daemons or servers.
|
|
4107
|
+
:raises: InsufficientAccountLimit, IntegrityError, InsufficientTargetRSEs, RSEOverQuota
|
|
4108
|
+
:attention: This method modifies the contents of the locks and replicas input parameters.
|
|
4109
|
+
"""
|
|
4110
|
+
|
|
4111
|
+
preferred_rse_ids = preferred_rse_ids or []
|
|
4112
|
+
source_rses = source_rses or []
|
|
4113
|
+
logger(logging.DEBUG, "Creating locks and replicas for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
|
|
4114
|
+
|
|
4115
|
+
replicas_to_create, locks_to_create, transfers_to_create = apply_rule_grouping(datasetfiles=datasetfiles,
|
|
4116
|
+
locks=locks,
|
|
4117
|
+
replicas=replicas,
|
|
4118
|
+
source_replicas=source_replicas,
|
|
4119
|
+
rseselector=rseselector,
|
|
4120
|
+
rule=rule,
|
|
4121
|
+
preferred_rse_ids=preferred_rse_ids,
|
|
4122
|
+
source_rses=source_rses,
|
|
4123
|
+
session=session)
|
|
4124
|
+
# Add the replicas
|
|
4125
|
+
session.add_all([item for sublist in replicas_to_create.values() for item in sublist])
|
|
4126
|
+
session.flush()
|
|
4127
|
+
|
|
4128
|
+
# Add the locks
|
|
4129
|
+
session.add_all([item for sublist in locks_to_create.values() for item in sublist])
|
|
4130
|
+
session.flush()
|
|
4131
|
+
|
|
4132
|
+
# Increase rse_counters
|
|
4133
|
+
for rse_id in replicas_to_create.keys():
|
|
4134
|
+
rse_counter.increase(rse_id=rse_id, files=len(replicas_to_create[rse_id]), bytes_=sum([replica.bytes for replica in replicas_to_create[rse_id]]), session=session)
|
|
4135
|
+
|
|
4136
|
+
# Increase account_counters
|
|
4137
|
+
for rse_id in locks_to_create.keys():
|
|
4138
|
+
account_counter.increase(rse_id=rse_id, account=rule.account, files=len(locks_to_create[rse_id]), bytes_=sum([lock.bytes for lock in locks_to_create[rse_id]]), session=session)
|
|
4139
|
+
|
|
4140
|
+
# Add the transfers
|
|
4141
|
+
logger(logging.DEBUG, "Rule %s [%d/%d/%d] queued %d transfers", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt, len(transfers_to_create))
|
|
4142
|
+
request_core.queue_requests(requests=transfers_to_create, session=session)
|
|
4143
|
+
session.flush()
|
|
4144
|
+
logger(logging.DEBUG, "Finished creating locks and replicas for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
|
|
4145
|
+
|
|
4146
|
+
|
|
4147
|
+
@transactional_session
|
|
4148
|
+
def __delete_lock_and_update_replica(
|
|
4149
|
+
lock: models.ReplicaLock,
|
|
4150
|
+
purge_replicas: bool = False,
|
|
4151
|
+
nowait: bool = False,
|
|
4152
|
+
*,
|
|
4153
|
+
session: "Session",
|
|
4154
|
+
logger: LoggerFunction = logging.log
|
|
4155
|
+
) -> bool:
|
|
4156
|
+
"""
|
|
4157
|
+
Delete a lock and update the associated replica.
|
|
4158
|
+
|
|
4159
|
+
:param lock: SQLAlchemy lock object.
|
|
4160
|
+
:param purge_replicas: Purge setting of the rule.
|
|
4161
|
+
:param nowait: The nowait option of the FOR UPDATE statement.
|
|
4162
|
+
:param session: The database session in use.
|
|
4163
|
+
:param logger: Optional decorated logger that can be passed from the calling daemons or servers.
|
|
4164
|
+
:returns: True, if the lock was replicating and the associated transfer should be canceled; False otherwise.
|
|
4165
|
+
"""
|
|
4166
|
+
|
|
4167
|
+
logger(logging.DEBUG, "Deleting lock %s:%s for rule %s", lock.scope, lock.name, str(lock.rule_id))
|
|
4168
|
+
lock.delete(session=session, flush=False)
|
|
4169
|
+
try:
|
|
4170
|
+
stmt = select(
|
|
4171
|
+
models.RSEFileAssociation
|
|
4172
|
+
).where(
|
|
4173
|
+
and_(models.RSEFileAssociation.scope == lock.scope,
|
|
4174
|
+
models.RSEFileAssociation.name == lock.name,
|
|
4175
|
+
models.RSEFileAssociation.rse_id == lock.rse_id)
|
|
4176
|
+
).with_for_update(
|
|
4177
|
+
nowait=nowait
|
|
4178
|
+
)
|
|
4179
|
+
replica = session.execute(stmt).scalar_one()
|
|
4180
|
+
replica.lock_cnt -= 1
|
|
4181
|
+
if replica.lock_cnt == 0:
|
|
4182
|
+
if purge_replicas:
|
|
4183
|
+
replica.tombstone = OBSOLETE
|
|
4184
|
+
elif replica.state == ReplicaState.UNAVAILABLE:
|
|
4185
|
+
replica.tombstone = OBSOLETE
|
|
4186
|
+
elif replica.accessed_at is not None:
|
|
4187
|
+
replica.tombstone = replica.accessed_at
|
|
4188
|
+
else:
|
|
4189
|
+
replica.tombstone = replica.created_at
|
|
4190
|
+
if lock.state == LockState.REPLICATING:
|
|
4191
|
+
replica.state = ReplicaState.UNAVAILABLE
|
|
4192
|
+
replica.tombstone = OBSOLETE
|
|
4193
|
+
return True
|
|
4194
|
+
except NoResultFound:
|
|
4195
|
+
logger(logging.ERROR, "Replica for lock %s:%s for rule %s on rse %s could not be found", lock.scope, lock.name, str(lock.rule_id), get_rse_name(rse_id=lock.rse_id, session=session))
|
|
4196
|
+
return False
|
|
4197
|
+
|
|
4198
|
+
|
|
4199
|
+
@transactional_session
|
|
4200
|
+
def __create_rule_approval_email(
|
|
4201
|
+
rule: models.ReplicationRule,
|
|
4202
|
+
*,
|
|
4203
|
+
session: "Session"
|
|
4204
|
+
) -> None:
|
|
4205
|
+
"""
|
|
4206
|
+
Create the rule notification email.
|
|
4207
|
+
|
|
4208
|
+
:param rule: The rule object.
|
|
4209
|
+
:param session: The database session in use.
|
|
4210
|
+
"""
|
|
4211
|
+
|
|
4212
|
+
filepath = path.join(
|
|
4213
|
+
config_get('common', 'mailtemplatedir'), 'rule_approval_request.tmpl' # type: ignore
|
|
4214
|
+
)
|
|
4215
|
+
with open(filepath, 'r') as templatefile:
|
|
4216
|
+
template = Template(templatefile.read())
|
|
4217
|
+
|
|
4218
|
+
did = rucio.core.did.get_did(
|
|
4219
|
+
scope=rule.scope,
|
|
4220
|
+
name=rule.name,
|
|
4221
|
+
dynamic_depth=DIDType.FILE,
|
|
4222
|
+
session=session
|
|
4223
|
+
)
|
|
4224
|
+
|
|
4225
|
+
reps = rucio.core.replica.list_dataset_replicas(
|
|
4226
|
+
scope=rule.scope, name=rule.name, session=session
|
|
4227
|
+
)
|
|
4228
|
+
rses = [rep['rse_id'] for rep in reps if rep['state'] == ReplicaState.AVAILABLE]
|
|
4229
|
+
|
|
4230
|
+
# RSE occupancy
|
|
4231
|
+
vo = rule.account.vo
|
|
4232
|
+
target_rses = parse_expression(rule.rse_expression, filter_={'vo': vo}, session=session)
|
|
4233
|
+
if len(target_rses) > 1:
|
|
4234
|
+
target_rse = 'Multiple'
|
|
4235
|
+
free_space = 'undefined'
|
|
4236
|
+
free_space_after = 'undefined'
|
|
4237
|
+
else:
|
|
4238
|
+
target_rse = target_rses[0]['rse']
|
|
4239
|
+
target_rse_id = target_rses[0]['id']
|
|
4240
|
+
free_space = 'undefined'
|
|
4241
|
+
free_space_after = 'undefined'
|
|
4242
|
+
|
|
4243
|
+
try:
|
|
4244
|
+
for usage in get_rse_usage(rse_id=target_rse_id, session=session):
|
|
4245
|
+
if usage['source'] == 'storage':
|
|
4246
|
+
free_space = sizefmt(usage['free'])
|
|
4247
|
+
if did['bytes'] is None:
|
|
4248
|
+
free_space_after = 'undefined'
|
|
4249
|
+
else:
|
|
4250
|
+
free_space_after = sizefmt(usage['free'] - did['bytes'])
|
|
4251
|
+
except Exception:
|
|
4252
|
+
pass
|
|
4253
|
+
|
|
4254
|
+
# Resolve recipients:
|
|
4255
|
+
recipients = _create_recipients_list(rse_expression=rule.rse_expression, filter_={'vo': vo}, session=session)
|
|
4256
|
+
|
|
4257
|
+
for recipient in recipients:
|
|
4258
|
+
text = template.safe_substitute(
|
|
4259
|
+
{
|
|
4260
|
+
'rule_id': str(rule.id),
|
|
4261
|
+
'created_at': str(rule.created_at),
|
|
4262
|
+
'expires_at': str(rule.expires_at),
|
|
4263
|
+
'account': rule.account.external,
|
|
4264
|
+
'email': get_account(account=rule.account, session=session).email,
|
|
4265
|
+
'rse_expression': rule.rse_expression,
|
|
4266
|
+
'comment': rule.comments,
|
|
4267
|
+
'scope': rule.scope.external,
|
|
4268
|
+
'name': rule.name,
|
|
4269
|
+
'did_type': rule.did_type,
|
|
4270
|
+
'length': '0' if did['length'] is None else str(did['length']),
|
|
4271
|
+
'bytes': '0' if did['bytes'] is None else sizefmt(did['bytes']),
|
|
4272
|
+
'open': did.get('open', 'Not Applicable'),
|
|
4273
|
+
'complete_rses': ', '.join(rses),
|
|
4274
|
+
'approvers': ','.join([r[0] for r in recipients]),
|
|
4275
|
+
'approver': recipient[1],
|
|
4276
|
+
'target_rse': target_rse,
|
|
4277
|
+
'free_space': free_space,
|
|
4278
|
+
'free_space_after': free_space_after
|
|
4279
|
+
}
|
|
4280
|
+
)
|
|
4281
|
+
|
|
4282
|
+
add_message(event_type='email',
|
|
4283
|
+
payload={'body': text,
|
|
4284
|
+
'to': [recipient[0]],
|
|
4285
|
+
'subject': '[RUCIO] Request to approve replication rule %s' % (str(rule.id))},
|
|
4286
|
+
session=session)
|
|
4287
|
+
|
|
4288
|
+
|
|
4289
|
+
@transactional_session
|
|
4290
|
+
def _create_recipients_list(
|
|
4291
|
+
rse_expression: str,
|
|
4292
|
+
filter_: Optional[str] = None,
|
|
4293
|
+
*,
|
|
4294
|
+
session: "Session"
|
|
4295
|
+
) -> list[tuple[str, Union[str, InternalAccount]]]:
|
|
4296
|
+
"""
|
|
4297
|
+
Create a list of recipients for a notification email based on rse_expression.
|
|
4298
|
+
|
|
4299
|
+
:param rse_expression: The rse_expression.
|
|
4300
|
+
:param session: The database session in use.
|
|
4301
|
+
"""
|
|
4302
|
+
|
|
4303
|
+
recipients: list[tuple] = [] # (eMail, account)
|
|
4304
|
+
|
|
4305
|
+
# APPROVERS-LIST
|
|
4306
|
+
# If there are accounts in the approvers-list of any of the RSEs only these should be used
|
|
4307
|
+
for rse in parse_expression(rse_expression, filter_=filter_, session=session):
|
|
4308
|
+
rse_attr = list_rse_attributes(rse_id=rse['id'], session=session)
|
|
4309
|
+
if rse_attr.get(RseAttr.RULE_APPROVERS):
|
|
4310
|
+
for account in rse_attr.get(RseAttr.RULE_APPROVERS).split(','):
|
|
4311
|
+
account = InternalAccount(account)
|
|
4312
|
+
try:
|
|
4313
|
+
email = get_account(account=account, session=session).email
|
|
4314
|
+
if email:
|
|
4315
|
+
recipients.append((email, account))
|
|
4316
|
+
except Exception:
|
|
4317
|
+
pass
|
|
4318
|
+
|
|
4319
|
+
# LOCALGROUPDISK/LOCALGROUPTAPE
|
|
4320
|
+
if not recipients:
|
|
4321
|
+
for rse in parse_expression(rse_expression, filter_=filter_, session=session):
|
|
4322
|
+
rse_attr = list_rse_attributes(rse_id=rse['id'], session=session)
|
|
4323
|
+
if rse_attr.get(RseAttr.TYPE, '') in ('LOCALGROUPDISK', 'LOCALGROUPTAPE'):
|
|
4324
|
+
|
|
4325
|
+
query = select(
|
|
4326
|
+
models.AccountAttrAssociation.account
|
|
4327
|
+
).where(
|
|
4328
|
+
models.AccountAttrAssociation.key == f'country-{rse_attr.get(RseAttr.COUNTRY, "")}',
|
|
4329
|
+
models.AccountAttrAssociation.value == 'admin'
|
|
4330
|
+
)
|
|
4331
|
+
|
|
4332
|
+
for account in session.execute(query).scalars().all():
|
|
4333
|
+
try:
|
|
4334
|
+
email = get_account(account=account, session=session).email
|
|
4335
|
+
if email:
|
|
4336
|
+
recipients.append((email, account))
|
|
4337
|
+
except Exception:
|
|
4338
|
+
pass
|
|
4339
|
+
|
|
4340
|
+
# GROUPDISK
|
|
4341
|
+
if not recipients:
|
|
4342
|
+
for rse in parse_expression(rse_expression, filter_=filter_, session=session):
|
|
4343
|
+
rse_attr = list_rse_attributes(rse_id=rse['id'], session=session)
|
|
4344
|
+
if rse_attr.get(RseAttr.TYPE, '') == 'GROUPDISK':
|
|
4345
|
+
|
|
4346
|
+
query = select(
|
|
4347
|
+
models.AccountAttrAssociation.account
|
|
4348
|
+
).where(
|
|
4349
|
+
models.AccountAttrAssociation.key == f'group-{rse_attr.get(RseAttr.PHYSGROUP, "")}',
|
|
4350
|
+
models.AccountAttrAssociation.value == 'admin'
|
|
4351
|
+
)
|
|
4352
|
+
|
|
4353
|
+
for account in session.execute(query).scalars().all():
|
|
4354
|
+
try:
|
|
4355
|
+
email = get_account(account=account, session=session).email
|
|
4356
|
+
if email:
|
|
4357
|
+
recipients.append((email, account))
|
|
4358
|
+
except Exception:
|
|
4359
|
+
pass
|
|
4360
|
+
|
|
4361
|
+
# DDMADMIN as default
|
|
4362
|
+
if not recipients:
|
|
4363
|
+
default_mail_from = config_get(
|
|
4364
|
+
'core', 'default_mail_from', raise_exception=False, default=None
|
|
4365
|
+
)
|
|
4366
|
+
if default_mail_from:
|
|
4367
|
+
recipients = [(default_mail_from, 'ddmadmin')]
|
|
4368
|
+
|
|
4369
|
+
return list(set(recipients))
|
|
4370
|
+
|
|
4371
|
+
|
|
4372
|
+
def __progress_class(replicating_locks, total_locks):
|
|
4373
|
+
"""
|
|
4374
|
+
Returns the progress class (10%, 20%, ...) of currently replicating locks.
|
|
4375
|
+
|
|
4376
|
+
:param replicating_locks: Currently replicating locks.
|
|
4377
|
+
:param total_locks: Total locks.
|
|
4378
|
+
"""
|
|
4379
|
+
|
|
4380
|
+
try:
|
|
4381
|
+
return int(float(total_locks - replicating_locks) / float(total_locks) * 10) * 10
|
|
4382
|
+
except Exception:
|
|
4383
|
+
return 0
|
|
4384
|
+
|
|
4385
|
+
|
|
4386
|
+
@policy_filter
|
|
4387
|
+
@transactional_session
|
|
4388
|
+
def archive_localgroupdisk_datasets(
|
|
4389
|
+
scope: InternalScope,
|
|
4390
|
+
name: str,
|
|
4391
|
+
*,
|
|
4392
|
+
session: "Session",
|
|
4393
|
+
logger: LoggerFunction = logging.log
|
|
4394
|
+
) -> None:
|
|
4395
|
+
"""
|
|
4396
|
+
ATLAS policy to archive a dataset which has a replica on LOCALGROUPDISK
|
|
4397
|
+
|
|
4398
|
+
:param scope: Scope of the dataset.
|
|
4399
|
+
:param name: Name of the dataset.
|
|
4400
|
+
:param session: The database session in use.
|
|
4401
|
+
:param logger: Optional decorated logger that can be passed from the calling daemons or servers.
|
|
4402
|
+
"""
|
|
4403
|
+
|
|
4404
|
+
rses_to_rebalance = []
|
|
4405
|
+
|
|
4406
|
+
archive = InternalScope('archive', vo=scope.vo)
|
|
4407
|
+
# Check if the archival dataset already exists
|
|
4408
|
+
try:
|
|
4409
|
+
rucio.core.did.get_did(scope=archive, name=name, session=session)
|
|
4410
|
+
return
|
|
4411
|
+
except DataIdentifierNotFound:
|
|
4412
|
+
pass
|
|
4413
|
+
|
|
4414
|
+
# Check if the dataset has a rule on a LOCALGROUPDISK
|
|
4415
|
+
for lock in rucio.core.lock.get_dataset_locks(scope=scope, name=name, session=session):
|
|
4416
|
+
if 'LOCALGROUPDISK' in lock['rse']:
|
|
4417
|
+
rses_to_rebalance.append({'rse_id': lock['rse_id'], 'rse': lock['rse'], 'account': lock['account']})
|
|
4418
|
+
# Remove duplicates from list
|
|
4419
|
+
rses_to_rebalance = [dict(t) for t in set([tuple(sorted(d.items())) for d in rses_to_rebalance])]
|
|
4420
|
+
|
|
4421
|
+
# There is at least one rule on LOCALGROUPDISK
|
|
4422
|
+
if rses_to_rebalance:
|
|
4423
|
+
content = [x for x in rucio.core.did.list_content(scope=scope, name=name, session=session)]
|
|
4424
|
+
if content:
|
|
4425
|
+
# Create the archival dataset
|
|
4426
|
+
did = rucio.core.did.get_did(scope=scope, name=name, session=session)
|
|
4427
|
+
meta = rucio.core.did.get_metadata(scope=scope, name=name, session=session)
|
|
4428
|
+
new_meta = {k: v for k, v in meta.items() if k in ['project', 'datatype', 'run_number', 'stream_name', 'prod_step', 'version', 'campaign', 'task_id', 'panda_id'] and v is not None}
|
|
4429
|
+
rucio.core.did.add_did(scope=archive,
|
|
4430
|
+
name=name,
|
|
4431
|
+
did_type=DIDType.DATASET,
|
|
4432
|
+
account=did['account'],
|
|
4433
|
+
statuses={},
|
|
4434
|
+
meta=new_meta,
|
|
4435
|
+
rules=[],
|
|
4436
|
+
lifetime=None,
|
|
4437
|
+
dids=[],
|
|
4438
|
+
rse_id=None,
|
|
4439
|
+
session=session)
|
|
4440
|
+
rucio.core.did.attach_dids(scope=archive, name=name, dids=content, account=did['account'], session=session)
|
|
4441
|
+
if not did['open']:
|
|
4442
|
+
rucio.core.did.set_status(scope=archive, name=name, open=False, session=session)
|
|
4443
|
+
|
|
4444
|
+
for rse in rses_to_rebalance:
|
|
4445
|
+
add_rule(dids=[{'scope': archive, 'name': name}],
|
|
4446
|
+
account=rse['account'],
|
|
4447
|
+
copies=1,
|
|
4448
|
+
rse_expression=rse['rse'],
|
|
4449
|
+
grouping='DATASET',
|
|
4450
|
+
weight=None,
|
|
4451
|
+
lifetime=None,
|
|
4452
|
+
locked=False,
|
|
4453
|
+
subscription_id=None,
|
|
4454
|
+
ignore_account_limit=True,
|
|
4455
|
+
ignore_availability=True,
|
|
4456
|
+
session=session)
|
|
4457
|
+
logger(logging.DEBUG, 'Re-Scoped %s:%s', scope, name)
|
|
4458
|
+
|
|
4459
|
+
|
|
4460
|
+
@policy_filter
|
|
4461
|
+
@read_session
|
|
4462
|
+
def get_scratch_policy(
|
|
4463
|
+
account: InternalAccount,
|
|
4464
|
+
rses: 'Sequence[dict[str, Any]]',
|
|
4465
|
+
lifetime: Optional[int],
|
|
4466
|
+
*,
|
|
4467
|
+
session: "Session"
|
|
4468
|
+
) -> Optional[int]:
|
|
4469
|
+
"""
|
|
4470
|
+
ATLAS policy for rules on SCRATCHDISK
|
|
4471
|
+
|
|
4472
|
+
:param account: Account of the rule.
|
|
4473
|
+
:param rses: List of RSEs.
|
|
4474
|
+
:param lifetime: Lifetime.
|
|
4475
|
+
:param session: The database session in use.
|
|
4476
|
+
"""
|
|
4477
|
+
|
|
4478
|
+
scratchdisk_lifetime = get_scratchdisk_lifetime()
|
|
4479
|
+
# Check SCRATCHDISK Policy
|
|
4480
|
+
if not has_account_attribute(account=account, key='admin', session=session) and (lifetime is None or lifetime > 60 * 60 * 24 * scratchdisk_lifetime):
|
|
4481
|
+
# Check if one of the rses is a SCRATCHDISK:
|
|
4482
|
+
if [rse for rse in rses if list_rse_attributes(rse_id=rse['id'], session=session).get(RseAttr.TYPE) == 'SCRATCHDISK']:
|
|
4483
|
+
lifetime = 60 * 60 * 24 * scratchdisk_lifetime - 1
|
|
4484
|
+
return lifetime
|