Repository: rucio/rucio

Issue:
Bug in delete_dids when bad files are declared
Motivation
------

Traceback :
```
  | Traceback (most recent call last):   File "/usr/local/lib/python3.6/site-packages/rucio/daemons/undertaker/undertaker.py", line 101, in undertaker     delete_dids(dids=chunk, account=InternalAccount('root', vo='def'), expire_rules=True)   File "/usr/local/lib/python3.6/site-packages/rucio/db/sqla/session.py", line 376, in new_funct     result = function(*args, **kwargs)   File "/usr/local/lib/python3.6/site-packages/rucio/core/did.py", line 757, in delete_dids     filter(or_(bad_replicas_clause)).\   File "<string>", line 2, in or_   File "/usr/local/lib64/python3.6/site-packages/sqlalchemy/sql/elements.py", line 2661, in or_     operators.or_, False_._singleton, True_._singleton, *clauses   File "/usr/local/lib64/python3.6/site-packages/sqlalchemy/sql/elements.py", line 2481, in _construct     for clause in util.coerce_generator_arg(clauses)   File "/usr/local/lib64/python3.6/site-packages/sqlalchemy/sql/elements.py", line 2481, in <listcomp>     for clause in util.coerce_generator_arg(clauses)   File "/usr/local/lib64/python3.6/site-packages/sqlalchemy/sql/coercions.py", line 189, in expect     element, argname=argname, **kw   File "/usr/local/lib64/python3.6/site-packages/sqlalchemy/sql/coercions.py", line 401, in _literal_coercion     self._raise_for_expected(element, argname)   File "/usr/local/lib64/python3.6/site-packages/sqlalchemy/sql/coercions.py", line 283, in _raise_for_expected     util.raise_(exc.ArgumentError(msg, code=code), replace_context=err)   File "/usr/local/lib64/python3.6/site-packages/sqlalchemy/util/compat.py", line 207, in raise_     raise exception sqlalchemy.exc.ArgumentError: SQL expression for WHERE/HAVING role expected, got [<sqlalchemy.sql.elements.BooleanClauseList object at 0x7fe464b1dbe0>].
-- | --
```





Discussion:

Resolving patch:
diff --git a/lib/rucio/core/did.py b/lib/rucio/core/did.py
--- a/lib/rucio/core/did.py
+++ b/lib/rucio/core/did.py
@@ -725,6 +725,10 @@ def delete_dids(dids, account, expire_rules=False, session=None, logger=logging.
                     filter(models.RSEFileAssociation.tombstone != none_value).\
                     update({'tombstone': datetime(1970, 1, 1)}, synchronize_session=False)
 
+    # Get bad files from dataset content
+    if file_content_clause:
+        bad_replicas_clause.extend([and_(models.BadReplicas.scope == cont['child_scope'], models.BadReplicas.name == cont['child_name']) for cont in session.query(models.DataIdentifierAssociation).filter(or_(*file_content_clause))])
+
     # Remove content
     if content_clause:
         with record_timer_block('undertaker.content'):
@@ -754,7 +758,7 @@ def delete_dids(dids, account, expire_rules=False, session=None, logger=logging.
     # Update bad_replicas if exist
     if bad_replicas_clause:
         rowcount = session.query(models.BadReplicas).\
-            filter(or_(bad_replicas_clause)).\
+            filter(or_(*bad_replicas_clause)).\
             filter(models.BadReplicas.state == BadFilesStatus.BAD).\
             update({'state': BadFilesStatus.DELETED, 'updated_at': datetime.utcnow()})
 


Test patch:
diff --git a/lib/rucio/tests/test_bad_replica.py b/lib/rucio/tests/test_bad_replica.py
--- a/lib/rucio/tests/test_bad_replica.py
+++ b/lib/rucio/tests/test_bad_replica.py
@@ -25,11 +25,12 @@
 
 from rucio.common.exception import RucioException, UnsupportedOperation, InvalidType
 from rucio.common.utils import generate_uuid, clean_surls
-from rucio.core.replica import add_replicas, get_replicas_state, list_replicas, declare_bad_file_replicas, list_bad_replicas, get_bad_pfns, get_bad_replicas_backlog
+from rucio.core.did import delete_dids
+from rucio.core.replica import add_replicas, get_replicas_state, list_replicas, declare_bad_file_replicas, list_bad_replicas, get_bad_pfns, get_bad_replicas_backlog, list_bad_replicas_status
 from rucio.daemons.badreplicas.minos import run as minos_run
 from rucio.daemons.badreplicas.minos_temporary_expiration import run as minos_temp_run
 from rucio.daemons.badreplicas.necromancer import run as necromancer_run
-from rucio.db.sqla.constants import DIDType, ReplicaState, BadPFNStatus
+from rucio.db.sqla.constants import DIDType, ReplicaState, BadPFNStatus, BadFilesStatus
 from rucio.tests.common import headers, auth
 
 
@@ -422,3 +423,36 @@ def test_client_add_temporary_unavailable_pfns(rse_factory, mock_scope, replica_
     for did in files:
         rep = get_replicas_state(scope=mock_scope, name=did['name'])
         assert list(rep.keys())[0] == ReplicaState.AVAILABLE
+
+
+def test_add_and_delete_bad_replicas(rse_factory, mock_scope, root_account, did_client, vo):
+    """ REPLICA (CORE): Add bad replicas and delete them"""
+    # Adding replicas to deterministic RSE
+    nbfiles = 5
+    rse1, rse1_id = rse_factory.make_srm_rse(deterministic=True, vo=vo)
+    files = [{'scope': mock_scope, 'name': 'file_%s' % generate_uuid(), 'bytes': 1, 'adler32': '0cc737eb', 'meta': {'events': 10}} for _ in range(nbfiles)]
+    client_files = [{'scope': file_['scope'].external, 'name': file_['name']} for file_ in files]
+    add_replicas(rse_id=rse1_id, files=files, account=root_account, ignore_availability=True)
+    tmp_dsn = 'dataset_%s' % generate_uuid()
+    did_client.add_dataset(scope=mock_scope.external, name=tmp_dsn)
+    did_client.add_files_to_dataset(mock_scope.external, name=tmp_dsn, files=client_files, rse=rse1)
+
+    # Declare replica bad
+    replicas = []
+    for replica in list_replicas(dids=[{'scope': f['scope'], 'name': f['name'], 'type': DIDType.FILE} for f in files], schemes=['srm']):
+        replicas.extend(replica['rses'][rse1_id])
+    r = declare_bad_file_replicas(replicas, 'This is a good reason', root_account)
+    assert r == {}
+
+    # Check state of bad replicas
+    list_bad_rep = [{'scope': rep['scope'].external, 'name': rep['name']} for rep in list_bad_replicas_status(state=BadFilesStatus.BAD, rse_id=rse1_id, vo=vo)]
+    for rep in client_files:
+        assert rep in list_bad_rep
+    assert [rep for rep in list_bad_replicas_status(state=BadFilesStatus.DELETED, rse_id=rse1_id, vo=vo)] == []
+
+    # Now delete the dataset
+    delete_dids([{'scope': mock_scope, 'name': tmp_dsn, 'did_type': DIDType.DATASET, 'purge_replicas': True}], account=root_account)
+    assert [rep for rep in list_bad_replicas_status(state=BadFilesStatus.BAD, rse_id=rse1_id, vo=vo)] == []
+    list_deleted_rep = [{'scope': rep['scope'].external, 'name': rep['name']} for rep in list_bad_replicas_status(state=BadFilesStatus.DELETED, rse_id=rse1_id, vo=vo)]
+    for rep in client_files:
+        assert rep in list_deleted_rep