internlm/SWE-Fixer-Train-110K huggingface dataset:
Repository: rucio/rucio

Issue:
Daemon crashes in 1.20.3rc1
Motivation
----------
Conveyor submitter raised an error -> core.transfer.get_transfer_requests_and_source_replicas
Light reaper raised an error -> Invalid UUID

Modification
------------
Reorder checks in get_transfer_requests_and_source_replicas
Convert rse name to id in light reaper (and dark reaper)



Discussion:

Resolving patch:
diff --git a/lib/rucio/vcsversion.py b/lib/rucio/vcsversion.py
--- a/lib/rucio/vcsversion.py
+++ b/lib/rucio/vcsversion.py
@@ -4,8 +4,8 @@
 '''
 VERSION_INFO = {
     'final': True,
-    'version': '1.20.3rc1',
-    'branch_nick': 'patch-0-Release__1_20_3rc1_preparation',
-    'revision_id': '374e51a371a9c6ae8f7dcc7e2a90e5665bc3a65f',
-    'revno': 8404
+    'version': '1.20.3rc2',
+    'branch_nick': 'patch-0-Release__1_20_3rc2_preparation',
+    'revision_id': 'b6e99b6a0b7b7c753dc12314c53406d8fe5604e0',
+    'revno': 8408
 }


Test patch:
Repository: rucio/rucio

Issue:
Dark Reaper rse_id crash
Motivation
----------
The dark reaper raises an error in 1.20.3


Modification
------------
Fix the query in core/quarantined_replica.py -> list_rses()




Discussion:

Resolving patch:
diff --git a/lib/rucio/core/quarantined_replica.py b/lib/rucio/core/quarantined_replica.py
--- a/lib/rucio/core/quarantined_replica.py
+++ b/lib/rucio/core/quarantined_replica.py
@@ -161,7 +161,7 @@ def list_rses(session=None):
 
     :returns: a list of RSEs.
     """
-    query = session.query(models.RSE.rse_id).distinct(models.RSE.rse_id).\
+    query = session.query(models.RSE.id).distinct(models.RSE.id).\
         filter(models.QuarantinedReplica.rse_id == models.RSE.id).\
         filter(models.RSE.deleted == false())
     return [rse for (rse,) in query]


Test patch:
Repository: rucio/rucio

Issue:
Dark Reaper rse_id crash
Motivation
----------
The dark reaper raises an error in 1.20.3


Modification
------------
Fix the query in core/quarantined_replica.py -> list_rses()




Discussion:

Resolving patch:
diff --git a/lib/rucio/core/quarantined_replica.py b/lib/rucio/core/quarantined_replica.py
--- a/lib/rucio/core/quarantined_replica.py
+++ b/lib/rucio/core/quarantined_replica.py
@@ -161,7 +161,7 @@ def list_rses(session=None):
 
     :returns: a list of RSEs.
     """
-    query = session.query(models.RSE.rse_id).distinct(models.RSE.rse_id).\
+    query = session.query(models.RSE.id).distinct(models.RSE.id).\
         filter(models.QuarantinedReplica.rse_id == models.RSE.id).\
         filter(models.RSE.deleted == false())
     return [rse for (rse,) in query]
diff --git a/lib/rucio/rse/protocols/gfal.py b/lib/rucio/rse/protocols/gfal.py
--- a/lib/rucio/rse/protocols/gfal.py
+++ b/lib/rucio/rse/protocols/gfal.py
@@ -394,7 +394,7 @@ def __gfal2_copy(self, src, dest, src_spacetoken=None, dest_spacetoken=None, tra
             params.timeout = int(transfer_timeout)
             watchdog = Timer(params.timeout + 60, self.__gfal2_cancel)
 
-        if not self.renaming and dest[:5] == 'https':
+        if not (self.renaming and dest[:5] == 'https'):
             dir_name = os.path.dirname(dest)
             # This function will be removed soon. gfal2 will create parent dir automatically.
             try:


Test patch:
Repository: rucio/rucio

Issue:
Minos-temporary-expiration does for update without nowait
Motivation
----------
The **minos-temporary-expiration** daemon does `update_replica_states` queries with `nowait=False`, it would be better with `nowait=True`. An even better solution might be to change the core query in https://github.com/rucio/rucio/blob/fa18f8298b7dacbee8d721542dbcf03aea6e3c93/lib/rucio/core/replica.py#L1748
to not call the `successful_transfer` lock update in case of transition from `TEMP_UNAVAILABLE` to `AVAILABLE`, since no transfers have to be marked.





Discussion:

Resolving patch:
diff --git a/lib/rucio/daemons/badreplicas/minos_temporary_expiration.py b/lib/rucio/daemons/badreplicas/minos_temporary_expiration.py
--- a/lib/rucio/daemons/badreplicas/minos_temporary_expiration.py
+++ b/lib/rucio/daemons/badreplicas/minos_temporary_expiration.py
@@ -14,7 +14,7 @@
 #
 # Authors:
 # - Cedric Serfon <cedric.serfon@cern.ch>, 2018-2019
-# - Martin Barisits <martin.barisits@cern.ch>, 2018
+# - Martin Barisits <martin.barisits@cern.ch>, 2018-2019
 #
 # PY3K COMPATIBLE
 
@@ -98,7 +98,7 @@ def minos_tu_expiration(bulk=1000, once=False, sleep_time=60):
 
             for chunk in chunks(replicas, chunk_size):
                 # Process and update the replicas in chunks
-                update_replicas_states(chunk, nowait=False, session=session)
+                update_replicas_states(chunk, nowait=True, session=session)
             for chunk in chunks(bad_replicas, chunk_size):
                 # Remove the replicas from bad_replicas table in chunks
                 bulk_delete_bad_replicas(chunk, session=session)


Test patch:
Repository: rucio/rucio

Issue:
Account and Scope new types
Motivation
----------
For multi-vo the internal representation of scope and account will need to be different from the external representation. The translations for these should be done in a consistent way and this can be prepared beforehand.


Modification
------------
Create a new type for each of scope and account. 
Convert incoming account names and scopes to these types in the API directory so that multi-vo changes are more straight forward.
If normal strings are used in core, raise an error.



Discussion:

Resolving patch:
diff --git a/lib/rucio/vcsversion.py b/lib/rucio/vcsversion.py
--- a/lib/rucio/vcsversion.py
+++ b/lib/rucio/vcsversion.py
@@ -4,8 +4,8 @@
 '''
 VERSION_INFO = {
     'final': True,
-    'version': '1.20.3',
-    'branch_nick': 'patch-0-Release__Rucio_1_20_3_preparation',
-    'revision_id': 'f05e019f7178590718bf3f1eee415cc46cb59159',
-    'revno': 8410
+    'version': '1.20.4rc1',
+    'branch_nick': 'patch-0-Release__1_20_4rc1_preparation',
+    'revision_id': '525812b8f83f1069d38ab78aebedb732f21e77ec',
+    'revno': 8418
 }


Test patch:
Repository: rucio/rucio

Issue:
Release candidate 1.20.4rc1 is unstable
Motivation
----------
There are issues with the release candidate:
- reaper2: messages fail to serialize
- judge: hangs on some requests
- REST list_replicas: TypeError, keys must be a string

Modification
------------
Reaper2 messages should be external representations.
Judge is hanging as the string type new internaltypes use with the backend db is different. This causes the indexes to be ignored and a query can need to scan the full table. This type needs to be changed from unicode.




Discussion:

Resolving patch:
diff --git a/lib/rucio/daemons/reaper/reaper2.py b/lib/rucio/daemons/reaper/reaper2.py
--- a/lib/rucio/daemons/reaper/reaper2.py
+++ b/lib/rucio/daemons/reaper/reaper2.py
@@ -88,7 +88,7 @@ def delete_from_storage(replicas, prot, rse_info, staging_areas, prepend_str):
         for replica in replicas:
             # Physical deletion
             try:
-                deletion_dict = {'scope': replica['scope'],
+                deletion_dict = {'scope': replica['scope'].external,
                                  'name': replica['name'],
                                  'rse': rse_name,
                                  'file-size': replica['bytes'],
@@ -142,7 +142,7 @@ def delete_from_storage(replicas, prot, rse_info, staging_areas, prepend_str):
     except (ServiceUnavailable, RSEAccessDenied, ResourceTemporaryUnavailable) as error:
         for replica in replicas:
             logging.warning('%s Deletion NOACCESS of %s:%s as %s on %s: %s', prepend_str, replica['scope'], replica['name'], replica['pfn'], rse_name, str(error))
-            add_message('deletion-failed', {'scope': replica['scope'],
+            add_message('deletion-failed', {'scope': replica['scope'].external,
                                             'name': replica['name'],
                                             'rse': rse_name,
                                             'file-size': replica['bytes'],
diff --git a/lib/rucio/db/sqla/models.py b/lib/rucio/db/sqla/models.py
--- a/lib/rucio/db/sqla/models.py
+++ b/lib/rucio/db/sqla/models.py
@@ -46,7 +46,9 @@
                                      BadPFNStatus)
 from rucio.db.sqla.history import Versioned
 from rucio.db.sqla.session import BASE
-from rucio.db.sqla.types import GUID, BooleanString, JSON, InternalAccountString, InternalScopeString
+from rucio.db.sqla.types import GUID, BooleanString, JSON
+from rucio.db.sqla.types import InternalAccountString as _InternalAccountString
+from rucio.db.sqla.types import InternalScopeString as _InternalScopeString
 
 
 # Recipe to force str instead if unicode
@@ -56,6 +58,16 @@ def String(*arg, **kw):
     return _String(*arg, **kw)
 
 
+def InternalScopeString(*arg, **kw):
+    kw['convert_unicode'] = 'force'
+    return _InternalScopeString(*arg, **kw)
+
+
+def InternalAccountString(*arg, **kw):
+    kw['convert_unicode'] = 'force'
+    return _InternalAccountString(*arg, **kw)
+
+
 @compiles(Boolean, "oracle")
 def compile_binary_oracle(type_, compiler, **kw):
     return "NUMBER(1)"


Test patch:
Repository: rucio/rucio

Issue:
Release candidate 1.20.4rc1 is unstable
Motivation
----------
There are issues with the release candidate:
- reaper2: messages fail to serialize
- judge: hangs on some requests
- REST list_replicas: TypeError, keys must be a string

Modification
------------
Reaper2 messages should be external representations.
Judge is hanging as the string type new internaltypes use with the backend db is different. This causes the indexes to be ignored and a query can need to scan the full table. This type needs to be changed from unicode.




Discussion:

Resolving patch:
diff --git a/lib/rucio/vcsversion.py b/lib/rucio/vcsversion.py
--- a/lib/rucio/vcsversion.py
+++ b/lib/rucio/vcsversion.py
@@ -4,8 +4,8 @@
 '''
 VERSION_INFO = {
     'final': True,
-    'version': '1.20.4rc1',
-    'branch_nick': 'patch-0-Release__1_20_4rc1_preparation',
-    'revision_id': '525812b8f83f1069d38ab78aebedb732f21e77ec',
-    'revno': 8418
+    'version': '1.20.4rc2',
+    'branch_nick': 'patch-0-Release__1_20_4rc2_preparation',
+    'revision_id': '5fc76107eae39f97ffbcb83400ab8844f7b5f7a5',
+    'revno': 8427
 }


Test patch:
Repository: rucio/rucio

Issue:
1.20.4rc2 storm protocol bug
Motivation
----------
The Storm protocol in RSEManager returns the input lfn as the pfn in lfns2pfns. This causes a crash as an InternalScope is then used as a dictionary key in list_replicas.

Modification
------------
The lfns dictionary should be sanitised so that scope is returned as an external string.



Discussion:

Resolving patch:
diff --git a/lib/rucio/rse/protocols/storm.py b/lib/rucio/rse/protocols/storm.py
--- a/lib/rucio/rse/protocols/storm.py
+++ b/lib/rucio/rse/protocols/storm.py
@@ -43,8 +43,23 @@ def _get_path(self, scope, name):
         return '%s/%s' % (scope, name)
 
     def lfns2pfns(self, lfns):
-        """ In this case, just returns back lfn. """
-        return lfns
+        """ In this case, just returns back lfn with external scope. """
+        if type(lfns) == dict:
+            val = lfns.copy()
+            if 'scope' in val and val['scope'] is not None:
+                val['scope'] = val['scope'].external
+
+        elif type(lfns) == list:
+            val = []
+            for l in lfns:
+                v = l.copy()
+                if 'scope' in v and v['scope'] is not None:
+                    v['scope'] = v['scope'].external
+                val.append(v)
+
+        else:
+            val = lfns
+        return val
 
     def path2pfn(self, path):
         """


Test patch:
Repository: rucio/rucio

Issue:
1.20.4rc2 storm protocol bug
Motivation
----------
The Storm protocol in RSEManager returns the input lfn as the pfn in lfns2pfns. This causes a crash as an InternalScope is then used as a dictionary key in list_replicas.

Modification
------------
The lfns dictionary should be sanitised so that scope is returned as an external string.



Discussion:

Resolving patch:
diff --git a/lib/rucio/vcsversion.py b/lib/rucio/vcsversion.py
--- a/lib/rucio/vcsversion.py
+++ b/lib/rucio/vcsversion.py
@@ -4,8 +4,8 @@
 '''
 VERSION_INFO = {
     'final': True,
-    'version': '1.20.4rc2',
-    'branch_nick': 'patch-0-Release__1_20_4rc2_preparation',
-    'revision_id': '5fc76107eae39f97ffbcb83400ab8844f7b5f7a5',
-    'revno': 8427
+    'version': '1.20.4rc3',
+    'branch_nick': 'patch-0-Release__1_20_4rc3_prep',
+    'revision_id': 'fe0518d675a40da8d34fae3c505a3763cc669c12',
+    'revno': 8433
 }


Test patch:
Repository: rucio/rucio

Issue:
Reaper 2.0 Should Reap Greedy RSE's Without Needing MinFreeSpace/MaxBeingDeletedFiles Set
Motivation
----------
The current version of the Reaper 2.0 will not reap an RSE marked as greedy that does not have MinFreeSpace and MaxBeingDeletedFiles set. MinFreeSpace should not matter for a greedy RSE, and MaxBeingDeletedFiles is not used by the Reaper 2.


Modification
------------
Move the check for greedy mode in __check_rse_usage() to before the acquisition of RSE limits.




Discussion:

Resolving patch:
diff --git a/lib/rucio/daemons/reaper/reaper2.py b/lib/rucio/daemons/reaper/reaper2.py
--- a/lib/rucio/daemons/reaper/reaper2.py
+++ b/lib/rucio/daemons/reaper/reaper2.py
@@ -195,6 +195,14 @@ def __check_rse_usage(rse, rse_id, prepend_str):
     if result is NO_VALUE:
         max_being_deleted_files, needed_free_space, used, free, obsolete = None, None, None, None, None
 
+        # First of all check if greedy mode is enabled for this RSE
+        attributes = list_rse_attributes(rse_id=rse_id)
+        greedy = attributes.get('greedyDeletion', False)
+        if greedy:
+            result = (max_being_deleted_files, 1000000000000, used, free)
+            REGION.set('rse_usage_%s' % rse_id, result)
+            return result
+
         # Get RSE limits
         limits = get_rse_limits(rse_id=rse_id)
         if not limits and 'MinFreeSpace' not in limits and 'MaxBeingDeletedFiles' not in limits:
@@ -207,20 +215,12 @@ def __check_rse_usage(rse, rse_id, prepend_str):
 
         # Check from which sources to get used and total spaces
         # Default is storage
-        attributes = list_rse_attributes(rse_id=rse_id)
         source_for_total_space = attributes.get('sourceForTotalSpace', 'storage')
         source_for_used_space = attributes.get('sourceForUsedSpace', 'storage')
-        greedy = attributes.get('greedyDeletion', False)
 
         logging.debug('%s RSE: %s, source_for_total_space: %s, source_for_used_space: %s',
                       prepend_str, rse, source_for_total_space, source_for_used_space)
 
-        # First of all check if greedy mode is enabled for this RSE
-        if greedy:
-            result = (max_being_deleted_files, 1000000000000, used, free)
-            REGION.set('rse_usage_%s' % rse_id, result)
-            return result
-
         # Get total, used and obsolete space
         rse_usage = get_rse_usage(rse_id=rse_id)
         usage = [entry for entry in rse_usage if entry['source'] == 'obsolete']


Test patch:
Repository: rucio/rucio

Issue:
protocol_stat_retries in rsemanager is loaded as string instead of int
Motivation
----------
As it is loaded as string instead of int, the method https://github.com/rucio/rucio/blob/fffbe0919f179467f3e11cb4bcb3896906755122/lib/rucio/rse/rsemanager.py#L729 crashes on line 738.

Modification
------------
config_get_int should be used here, however, this method needs to be expanded with the raise_Exception and default options similar to config_get and config_get_bool



Discussion:

Resolving patch:
diff --git a/lib/rucio/common/config.py b/lib/rucio/common/config.py
--- a/lib/rucio/common/config.py
+++ b/lib/rucio/common/config.py
@@ -80,14 +80,24 @@ def config_add_section(section):
     return __CONFIG.add_section(section)
 
 
-def config_get_int(section, option):
+def config_get_int(section, option, raise_exception=True, default=None):
     """Return the integer value for a given option in a section"""
-    return __CONFIG.getint(section, option)
+    try:
+        return __CONFIG.getint(section, option)
+    except (ConfigParser.NoOptionError, ConfigParser.NoSectionError) as err:
+        if raise_exception and default is None:
+            raise err
+        return default
 
 
-def config_get_float(section, option):
+def config_get_float(section, option, raise_exception=True, default=None):
     """Return the floating point value for a given option in a section"""
-    return __CONFIG.getfloat(section, option)
+    try:
+        return __CONFIG.getfloat(section, option)
+    except (ConfigParser.NoOptionError, ConfigParser.NoSectionError) as err:
+        if raise_exception and default is None:
+            raise err
+        return default
 
 
 def config_get_bool(section, option, raise_exception=True, default=None):
diff --git a/lib/rucio/rse/rsemanager.py b/lib/rucio/rse/rsemanager.py
--- a/lib/rucio/rse/rsemanager.py
+++ b/lib/rucio/rse/rsemanager.py
@@ -42,7 +42,7 @@
     from urllib.parse import urlparse
 
 from rucio.common import exception, utils, constants
-from rucio.common.config import config_get
+from rucio.common.config import config_get_int
 from rucio.common.constraints import STRING_TYPES
 from rucio.common.utils import make_valid_did
 
@@ -733,7 +733,7 @@ def _retry_protocol_stat(protocol, pfn):
     :param protocol     The protocol to use to reach this file
     :param pfn          Physical file name of the target for the protocol stat
     """
-    retries = config_get('client', 'protocol_stat_retries', raise_exception=False, default=6)
+    retries = config_get_int('client', 'protocol_stat_retries', raise_exception=False, default=6)
 
     for attempt in range(retries):
         try:


Test patch:
Repository: rucio/rucio

Issue:
bug in resolving site attribute to only one RSE
Motivation
----------
in the new request listing API from #2878 the use of sites does not work properly as the sitename gets resolved to only one RSE.


Modification
------------
resolve sitename to all RSEs with the site attribute




Discussion:

Resolving patch:
diff --git a/lib/rucio/daemons/bb8/common.py b/lib/rucio/daemons/bb8/common.py
--- a/lib/rucio/daemons/bb8/common.py
+++ b/lib/rucio/daemons/bb8/common.py
@@ -19,6 +19,7 @@
 # - Cedric Serfon <cedric.serfon@cern.ch>, 2017
 # - Hannes Hansen <hannes.jakob.hansen@cern.ch>, 2018
 # - Andrew Lister <andrew.lister@stfc.ac.uk>, 2019
+# - Dimitrios Christidis <dimitrios.christidis@cern.ch>, 2019
 #
 # PY3K COMPATIBLE
 
@@ -41,6 +42,7 @@
 from rucio.common.config import config_get
 from rucio.common.exception import (InsufficientTargetRSEs, RuleNotFound, DuplicateRule,
                                     InsufficientAccountLimit)
+from rucio.common.types import InternalAccount
 
 from rucio.db.sqla.session import transactional_session
 from rucio.db.sqla import models
@@ -243,7 +245,7 @@ def list_rebalance_rule_candidates(rse, mode=None, session=None):
     # the rest is done with sql query
     from_date = datetime.utcnow() + timedelta(days=60)
     to_date = datetime.now() - timedelta(days=60)
-    allowed_accounts = ['panda', 'root', 'ddmadmin']
+    allowed_accounts = [InternalAccount(a) for a in ('panda', 'root', 'ddmadmin')]
     allowed_grouping = [RuleGrouping.DATASET, RuleGrouping.ALL]
     external_dsl = aliased(models.DatasetLock)
     count_locks = select([func.count()]).where(and_(external_dsl.scope == models.DatasetLock.scope,
@@ -331,7 +333,7 @@ def select_target_rse(parent_rule, current_rse, rse_expression, subscription_id,
     elif int(rse_attributes['tier']) == 3:
         # Tier 3 will go to Tier 2, since we don't have enough t3s
         rses = parse_expression(expression='((tier=2&type=DATADISK)\\datapolicynucleus=1)\\%s' % target_rse, filter={'availability_write': True}, session=session)
-    rseselector = RSESelector(account='ddmadmin', rses=rses, weight='freespace', copies=1, ignore_account_limit=True, session=session)
+    rseselector = RSESelector(account=InternalAccount('ddmadmin'), rses=rses, weight='freespace', copies=1, ignore_account_limit=True, session=session)
     return get_rse_name([rse_id for rse_id, _, _ in rseselector.select_rse(size=0, preferred_rse_ids=[], blacklist=other_rses)][0], session=session)
 
 


Test patch:
Repository: rucio/rucio

Issue:
declare_bad_file_replicas broken
Motivation
----------
`declare_bad_file_replicas` is broken after the move to `InternalAccount`



Discussion:

Resolving patch:
diff --git a/lib/rucio/daemons/conveyor/finisher.py b/lib/rucio/daemons/conveyor/finisher.py
--- a/lib/rucio/daemons/conveyor/finisher.py
+++ b/lib/rucio/daemons/conveyor/finisher.py
@@ -17,7 +17,7 @@
 # - Mario Lassnig <mario.lassnig@cern.ch>, 2015
 # - Vincent Garonne <vgaronne@gmail.com>, 2015-2018
 # - Martin Barisits <martin.barisits@cern.ch>, 2015-2019
-# - Cedric Serfon <cedric.serfon@cern.ch>, 2017-2018
+# - Cedric Serfon <cedric.serfon@cern.ch>, 2017-2019
 # - Hannes Hansen <hannes.jakob.hansen@cern.ch>, 2018
 # - Andrew Lister <andrew.lister@stfc.ac.uk>, 2019
 #
@@ -49,6 +49,7 @@
 from sqlalchemy.exc import DatabaseError
 
 from rucio.common.config import config_get
+from rucio.common.types import InternalAccount
 from rucio.common.utils import chunks
 from rucio.common.exception import DatabaseException, ConfigNotFound, UnsupportedOperation, ReplicaNotFound, RequestNotFound
 from rucio.core import request as request_core, heartbeat, replica as replica_core
@@ -351,7 +352,7 @@ def __check_suspicious_files(req, suspicious_patterns):
                     pfns.append(url['url'])
                 if pfns:
                     logging.debug("Found suspicious urls: %s", str(pfns))
-                    replica_core.declare_bad_file_replicas(pfns, reason=reason, issuer='root', status=BadFilesStatus.SUSPICIOUS)
+                    replica_core.declare_bad_file_replicas(pfns, reason=reason, issuer=InternalAccount('root'), status=BadFilesStatus.SUSPICIOUS)
     except Exception as error:
         logging.warning("Failed to check suspicious file with request: %s - %s", req['request_id'], str(error))
     return is_suspicious
@@ -444,7 +445,7 @@ def __update_replica(replica, session=None):
                                          replica['name'],
                                          replica['bytes'],
                                          pfn=replica['pfn'] if 'pfn' in replica else None,
-                                         account='root',  # it will deleted immediately, do we need to get the accurate account from rule?
+                                         account=InternalAccount('root'),  # it will deleted immediately, do we need to get the accurate account from rule?
                                          adler32=replica['adler32'],
                                          tombstone=datetime.datetime.utcnow(),
                                          session=session)


Test patch:
Repository: rucio/rucio

Issue:
Improperly Raising Exception in FTS3 Transfertool
Motivation
----------
File "/usr/lib/python2.7/site-packages/rucio/transfertool/fts3.py", line 221, in submit
raise('No transfer id returned by %s' % self.external_host)
TypeError: exceptions must be old-style classes or derived from BaseException, not unicode


Modification
------------
Needs to raise an actual exception class.




Discussion:

Resolving patch:
diff --git a/lib/rucio/transfertool/fts3.py b/lib/rucio/transfertool/fts3.py
--- a/lib/rucio/transfertool/fts3.py
+++ b/lib/rucio/transfertool/fts3.py
@@ -218,7 +218,7 @@ def submit(self, files, job_params, timeout=None):
             record_counter('transfertool.fts3.%s.submission.failure' % self.__extract_host(self.external_host), len(files))
 
         if not transfer_id:
-            raise('No transfer id returned by %s' % self.external_host)
+            raise TransferToolWrongAnswer('No transfer id returned by %s' % self.external_host)
         return transfer_id
 
     def cancel(self, transfer_ids, timeout=None):


Test patch: