Repository: rucio/rucio

Issue:
Heartbeat handler for automatix daemon
Motivation
----------
Heartbeat handler for automatix daemon (see https://github.com/rucio/rucio/issues/5562)




Discussion:

Resolving patch:
diff --git a/lib/rucio/daemons/automatix/automatix.py b/lib/rucio/daemons/automatix/automatix.py
--- a/lib/rucio/daemons/automatix/automatix.py
+++ b/lib/rucio/daemons/automatix/automatix.py
@@ -13,134 +13,51 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import functools
 import logging
 import random
-import socket
 import tempfile
 import threading
 from configparser import NoOptionError, NoSectionError
 from datetime import datetime
 from json import load
-from math import exp
-from os import remove, rmdir, stat, getpid
-from time import sleep, time
+from os import remove, rmdir
+from typing import TYPE_CHECKING
 
 import rucio.db.sqla.util
 from rucio.client import Client
+from rucio.client.uploadclient import UploadClient
 from rucio.common import exception
-from rucio.common.config import config_get
-from rucio.common.exception import FileReplicaAlreadyExists
-from rucio.common.logging import formatted_logger, setup_logging
+from rucio.common.config import config_get, config_get_int, config_get_bool
+from rucio.common.logging import setup_logging
 from rucio.common.types import InternalScope
-from rucio.common.utils import adler32, daemon_sleep
 from rucio.common.utils import execute, generate_uuid
-from rucio.core import monitor, heartbeat
+from rucio.core import monitor
 from rucio.core.scope import list_scopes
-from rucio.rse import rsemanager as rsemgr
+from rucio.core.vo import map_vo
+from rucio.daemons.common import run_daemon
 
-SUCCESS = 0
-FAILURE = 1
 
+if TYPE_CHECKING:
+    from rucio.daemons.common import HeartbeatHandler
 
-GRACEFUL_STOP = threading.Event()
+graceful_stop = threading.Event()
 
 
-def upload(files, scope, metadata, rse, account, source_dir, dataset_lifetime, did=None, set_metadata=False, logger=logging.log):
-    logger(logging.DEBUG, 'In upload')
-    dsn = None
-    if did:
-        dsn = {'scope': did.split(':')[0], 'name': did.split(':')[1]}
-    client = Client()
-
-    list_files = []
-    lfns = []
-    for filename in files:
-        physical_fname = filename
-        if physical_fname.find('/') > -1:
-            physical_fname = "".join(filename.split('/'))
-        fullpath = '%s/%s' % (source_dir, physical_fname)
-        size = stat(fullpath).st_size
-        checksum = adler32(fullpath)
-        logger(logging.INFO, 'File %s : Size %s , adler32 %s', fullpath, str(size), checksum)
-        list_files.append({'scope': scope, 'name': filename, 'bytes': size, 'adler32': checksum, 'meta': {'guid': generate_uuid()}})
-        lfns.append({'name': filename, 'scope': scope, 'filesize': size, 'adler32': checksum, 'filename': physical_fname})
-
-    # Physical upload
-    logger(logging.INFO, 'Uploading physically the files %s on %s', str(lfns), rse)
-    rse_info = rsemgr.get_rse_info(rse, vo=client.vo)
-    try:
-        success_upload = True
-        for cnt in range(0, 3):
-            rows = rsemgr.upload(rse_info, lfns=lfns, source_dir=source_dir, vo=client.vo, logger=logger)
-            # temporary hack
-            global_status, ret = rows['success'], rows[1]
-            logger(logging.INFO, 'Returned global status : %s, Returned : %s', str(global_status), str(ret))
-            if not global_status:
-                for item in ret:
-                    if (not isinstance(ret[item], FileReplicaAlreadyExists)) and ret[item] is not True:
-                        sleep(exp(cnt))
-                        success_upload = False
-                        logger(logging.ERROR, 'Problem to upload file %s with error %s', item, str(ret[item]))
-                        break
-            else:
-                break
-        if not success_upload:
-            logger(logging.ERROR, 'Upload operation to %s failed, removing leftovers', rse)
-            rsemgr.delete(rse_info, lfns=lfns)
-            return False
-    except Exception as error:
-        logger(logging.DEBUG, "Exception", exc_info=True)
-        logger(logging.ERROR, '%s', str(error))
-        return False
-    logger(logging.INFO, 'Files successfully copied on %s', rse)
-
-    # Registering DIDs and replicas in Rucio
-    logger(logging.INFO, 'Registering DIDs and replicas in Rucio')
-    meta = metadata
-    if not set_metadata:
-        meta = None
-    if dsn:
-        try:
-            client.add_dataset(scope=dsn['scope'], name=dsn['name'], rules=[{'account': account, 'copies': 1, 'rse_expression': rse, 'grouping': 'DATASET', 'activity': 'Functional Test'}], meta=meta, lifetime=dataset_lifetime)
-            client.add_files_to_dataset(scope=dsn['scope'], name=dsn['name'], files=list_files, rse=rse)
-            logger(logging.INFO, 'Upload operation for %s:%s done', dsn['scope'], dsn['name'])
-        except Exception as error:
-            logger(logging.DEBUG, "Exception", exc_info=True)
-            logger(logging.ERROR, 'Failed to upload %s', str(list_files))
-            logger(logging.ERROR, '%s', str(error))
-            logger(logging.ERROR, 'removing files from the Storage')
-            rsemgr.delete(rse_info, lfns=lfns)
-            return False
-    else:
-        logger(logging.WARNING, 'No dsn is specified')
-        try:
-            client.add_replicas(files=list_files, rse=rse)
-            client.add_replication_rule(list_files, copies=1, rse_expression=rse, activity='Functional Test')
-            logger(logging.INFO, 'Upload operation for %s done', str(list_files))
-        except Exception as error:
-            logger(logging.DEBUG, "Exception", exc_info=True)
-            logger(logging.ERROR, 'Failed to upload %s', str(list_files))
-            logger(logging.ERROR, '%s', str(error))
-            logger(logging.ERROR, 'Removing files from the Storage')
-            rsemgr.delete(rse_info, lfns=lfns)
-            return False
-    return True
-
-
-def get_data_distribution(inputfile):
+def get_data_distribution(inputfile: str):
     with open(inputfile) as data_file:
         data = load(data_file)
     probabilities = {}
     probability = 0
     for key in data:
-        probability += data[key]['probability']
+        probability += data[key]["probability"]
         probabilities[key] = probability
     for key in probabilities:
         probabilities[key] = float(probabilities[key]) / probability
     return probabilities, data
 
 
-def choose_element(probabilities, data):
+def choose_element(probabilities: dict, data: str) -> float:
     rnd = random.uniform(0, 1)
     prob = 0
     for key in probabilities:
@@ -150,183 +67,210 @@ def choose_element(probabilities, data):
     return data[key]
 
 
-def generate_file(fname, size, logger=logging.log):
-    cmd = '/bin/dd if=/dev/urandom of=%s bs=%s count=1' % (fname, size)
+def generate_file(fname: str, size: int, logger=logging.log) -> int:
+    cmd = "/bin/dd if=/dev/urandom of=%s bs=%s count=1" % (fname, size)
     exitcode, out, err = execute(cmd)
     logger(logging.DEBUG, out)
     logger(logging.DEBUG, err)
     return exitcode
 
 
-def generate_didname(metadata, dsn, did_type):
+def generate_didname(metadata: dict, dsn: str, did_type: str) -> str:
     try:
-        did_prefix = config_get('automatix', 'did_prefix')
+        did_prefix = config_get("automatix", "did_prefix")
     except (NoOptionError, NoSectionError, RuntimeError):
-        did_prefix = ''
+        did_prefix = ""
     try:
-        pattern = config_get('automatix', '%s_pattern' % did_type)
-        separator = config_get('automatix', 'separator')
+        pattern = config_get("automatix", "%s_pattern" % did_type)
+        separator = config_get("automatix", "separator")
     except (NoOptionError, NoSectionError, RuntimeError):
         return generate_uuid()
     fields = pattern.split(separator)
-    file_name = ''
+    file_name = ""
     for field in fields:
-        if field == 'date':
+        if field == "date":
             field_str = str(datetime.now().date())
-        elif field == 'did_prefix':
+        elif field == "did_prefix":
             field_str = did_prefix
-        elif field == 'dsn':
+        elif field == "dsn":
             field_str = dsn
-        elif field == 'uuid':
+        elif field == "uuid":
             field_str = generate_uuid()
-        elif field == 'randint':
+        elif field == "randint":
             field_str = str(random.randint(0, 100000))
         else:
             field_str = metadata.get(field, None)
             if not field_str:
                 field_str = str(random.randint(0, 100000))
-        file_name = '%s%s%s' % (file_name, separator, field_str)
+        file_name = "%s%s%s" % (file_name, separator, field_str)
     len_separator = len(separator)
     return file_name[len_separator:]
 
 
-def automatix(sites, inputfile, sleep_time, account, worker_number=1, total_workers=1, scope='tests', once=False, dataset_lifetime=None, set_metadata=False):
-    sleep(sleep_time * (total_workers - worker_number) / total_workers)
+def automatix(inputfile: str, sleep_time: int, once: bool = False) -> None:
+    """
+    Creates an automatix Worker that uploads datasets to a list of rses.
+
+    :param inputfile: The input file where the parameters of the distribution is set
+    :param sleep_time: Thread sleep time after each chunk of work.
+    :param once: Run only once.
+    """
+    run_daemon(
+        once=once,
+        graceful_stop=graceful_stop,
+        executable="automatix",
+        logger_prefix="automatix",
+        partition_wait_time=1,
+        sleep_time=sleep_time,
+        run_once_fnc=functools.partial(
+            run_once,
+            inputfile=inputfile,
+        ),
+    )
+
+
+def run_once(heartbeat_handler: "HeartbeatHandler", inputfile: str, **_kwargs) -> bool:
 
-    executable = 'automatix'
-    hostname = socket.getfqdn()
-    pid = getpid()
-    hb_thread = threading.current_thread()
-    heartbeat.sanity_check(executable=executable, hostname=hostname)
-    prefix = 'automatix[%i/%i] : ' % (worker_number, total_workers)
-    logger = formatted_logger(logging.log, prefix + '%s')
-    while not GRACEFUL_STOP.is_set():
-        heartbeat.live(executable, hostname, pid, hb_thread)
-        starttime = time()
-        prefix = 'automatix[%i/%i] : ' % (worker_number, total_workers)
-        logger = formatted_logger(logging.log, prefix + '%s')
-        logger(logging.INFO, 'Getting data distribution')
-        probabilities, data = get_data_distribution(inputfile)
-        logger(logging.DEBUG, 'Probabilities %s', probabilities)
-        totretries = 3
-        status = False
+    _, _, logger = heartbeat_handler.live()
+    try:
+        rses = [
+            s.strip() for s in config_get("automatix", "rses").split(",")
+        ]  # TODO use config_get_list
+    except (NoOptionError, NoSectionError, RuntimeError):
+        logging.log(
+            logging.ERROR,
+            "Option rses not found in automatix section. Trying the legacy sites option",
+        )
+        try:
+            rses = [
+                s.strip() for s in config_get("automatix", "sites").split(",")
+            ]  # TODO use config_get_list
+            logging.log(
+                logging.WARNING,
+                "Option sites found in automatix section. This option will be deprecated soon. Please update your config to use rses.",
+            )
+        except (NoOptionError, NoSectionError, RuntimeError):
+            logger(logging.ERROR, "Could not load sites from configuration")
+            return True
 
-        for site in sites:
+    set_metadata = config_get_bool(
+        "automatix", "set_metadata", raise_exception=False, default=True
+    )
+    dataset_lifetime = config_get_int(
+        "automatix", "dataset_lifetime", raise_exception=False, default=0
+    )
+    account = config_get("automatix", "account", raise_exception=False, default="root")
+    scope = config_get("automatix", "scope", raise_exception=False, default="test")
+    client = Client(account=account)
+    vo = map_vo(client.vo)
+    filters = {"scope": InternalScope("*", vo=vo)}
+    scopes = list_scopes(filter_=filters)
+    if InternalScope(scope, vo=vo) not in scopes:
+        logger(logging.ERROR, "Scope %s does not exist. Exiting", scope)
+        return True
 
-            for retry in range(0, totretries):
-                timer = monitor.Timer()
-                tmpdir = tempfile.mkdtemp()
-                logger(logging.INFO, 'Running on site %s', site)
-                dic = choose_element(probabilities, data)
-                metadata = dic['metadata']
-                try:
-                    nbfiles = dic['nbfiles']
-                except KeyError:
-                    nbfiles = 2
-                    logger(logging.WARNING, 'No nbfiles defined in the configuration, will use 2')
-                try:
-                    filesize = dic['filesize']
-                except KeyError:
-                    filesize = 1000000
-                    logger(logging.WARNING, 'No filesize defined in the configuration, will use 1M files')
-                dsn = generate_didname(metadata, None, 'dataset')
-                fnames = []
-                lfns = []
-                physical_fnames = []
-                for _ in range(nbfiles):
-                    fname = generate_didname(metadata=metadata, dsn=dsn, did_type='file')
-                    lfns.append(fname)
-                    logger(logging.INFO, 'Generating file %s in dataset %s', fname, dsn)
-                    physical_fname = '%s/%s' % (tmpdir, "".join(fname.split('/')))
-                    physical_fnames.append(physical_fname)
-                    generate_file(physical_fname, filesize, logger=logger)
-                    fnames.append(fname)
-                logger(logging.INFO, 'Upload %s to %s', dsn, site)
-                dsn = '%s:%s' % (scope, dsn)
-                status = upload(files=lfns, scope=scope, metadata=metadata, rse=site, account=account, source_dir=tmpdir, dataset_lifetime=dataset_lifetime, did=dsn, set_metadata=set_metadata, logger=logger)
-                for physical_fname in physical_fnames:
-                    remove(physical_fname)
-                rmdir(tmpdir)
-                if status:
-                    monitor.record_counter(name='automatix.addnewdataset.done', delta=1)
-                    monitor.record_counter(name='automatix.addnewfile.done', delta=nbfiles)
-                    timer.record('automatix.datasetinjection')
-                    break
-                else:
-                    logger(logging.INFO, 'Failed to upload files. Will retry another time (attempt %s/%s)', str(retry + 1), str(totretries))
-        if once is True:
-            logger(logging.INFO, 'Run with once mode. Exiting')
-            break
-        tottime = time() - starttime
-        if status:
-            logger(logging.INFO, 'It took %s seconds to upload one dataset on %s', str(tottime), str(sites))
-            daemon_sleep(start_time=starttime, sleep_time=sleep_time, graceful_stop=GRACEFUL_STOP, logger=logger)
+    logger(logging.INFO, "Getting data distribution")
+    probabilities, data = get_data_distribution(inputfile)
+    logger(logging.DEBUG, "Probabilities %s", probabilities)
+
+    cycle_timer = monitor.Timer()
+    for rse in rses:
+        timer = monitor.Timer()
+        _, _, logger = heartbeat_handler.live()
+        tmpdir = tempfile.mkdtemp()
+        logger(logging.INFO, "Running on RSE %s", rse)
+        dic = choose_element(probabilities, data)
+        metadata = dic["metadata"]
+        try:
+            nbfiles = dic["nbfiles"]
+        except KeyError:
+            nbfiles = 2
+            logger(
+                logging.WARNING, "No nbfiles defined in the configuration, will use 2"
+            )
+        try:
+            filesize = dic["filesize"]
+        except KeyError:
+            filesize = 1000000
+            logger(
+                logging.WARNING,
+                "No filesize defined in the configuration, will use 1M files",
+            )
+        dsn = generate_didname(metadata, None, "dataset")
+        fnames = []
+        lfns = []
+        physical_fnames = []
+        files = []
+        for _ in range(nbfiles):
+            fname = generate_didname(metadata=metadata, dsn=dsn, did_type="file")
+            lfns.append(fname)
+            logger(logging.INFO, "Generating file %s in dataset %s", fname, dsn)
+            physical_fname = "%s/%s" % (tmpdir, "".join(fname.split("/")))
+            physical_fnames.append(physical_fname)
+            generate_file(physical_fname, filesize, logger=logger)
+            fnames.append(fname)
+            file_ = {
+                "did_scope": scope,
+                "did_name": fname,
+                "dataset_scope": scope,
+                "dataset_name": dsn,
+                "rse": rse,
+                "path": physical_fname,
+            }
+            files.append(file_)
+        logger(logging.INFO, "Upload %s:%s to %s", scope, dsn, rse)
+        upload_client = UploadClient(client)
+        ret = upload_client.upload(files)
+        if ret == 0:
+            logger(logging.INFO, "%s sucessfully registered" % dsn)
+            if set_metadata:
+                client.set_metadata_bulk(
+                    scope=scope, name=dsn, meta=metadata, recursive=False
+                )
+                monitor.record_counter(name="automatix.addnewdataset.done", delta=1)
+                monitor.record_counter(name="automatix.addnewfile.done", delta=nbfiles)
+                timer.record('automatix.datasetinjection')
+            if dataset_lifetime:
+                client.set_metadata(
+                    scope=scope, name=dsn, key="lifetime", value=dataset_lifetime
+                )
         else:
-            logger(logging.INFO, 'Retrying a new upload')
-    heartbeat.die(executable, hostname, pid, hb_thread)
-    logger(logging.INFO, 'Graceful stop requested')
-    logger(logging.INFO, 'Graceful stop done')
+            logger(logging.INFO, "Error uploading files")
+        for physical_fname in physical_fnames:
+            remove(physical_fname)
+        rmdir(tmpdir)
+    logger(
+        logging.INFO,
+        "It took %f seconds to upload one dataset on %s",
+        cycle_timer.elapsed,
+        str(rses),
+    )
+    return True
 
 
-def run(total_workers=1, once=False, inputfile=None, sleep_time=-1):
+def run(
+    total_workers: int = 1,
+    once: bool = False,
+    inputfile: str = "/opt/rucio/etc/automatix.json",
+    sleep_time: int = 60,
+) -> None:
     """
     Starts up the automatix threads.
     """
     setup_logging()
 
     if rucio.db.sqla.util.is_old_db():
-        raise exception.DatabaseException('Database was not updated, daemon won\'t start')
-
-    try:
-        sites = [s.strip() for s in config_get('automatix', 'sites').split(',')]
-    except (NoOptionError, NoSectionError, RuntimeError):
-        raise Exception('Could not load sites from configuration')
-    if not inputfile:
-        inputfile = '/opt/rucio/etc/automatix.json'
-    if sleep_time == -1:
-        try:
-            sleep_time = config_get('automatix', 'sleep_time')
-        except (NoOptionError, NoSectionError, RuntimeError):
-            sleep_time = 30
-    try:
-        account = config_get('automatix', 'account')
-    except (NoOptionError, NoSectionError, RuntimeError):
-        account = 'root'
-    try:
-        dataset_lifetime = config_get('automatix', 'dataset_lifetime')
-    except (NoOptionError, NoSectionError, RuntimeError):
-        dataset_lifetime = None
-    try:
-        set_metadata = config_get('automatix', 'set_metadata')
-    except (NoOptionError, NoSectionError, RuntimeError):
-        set_metadata = False
-
-    try:
-        scope = config_get('automatix', 'scope')
-        client = Client()
-        filters = {'scope': InternalScope('*', vo=client.vo)}
-        if InternalScope(scope, vo=client.vo) not in list_scopes(filter_=filters):
-            logging.log(logging.ERROR, 'Scope %s does not exist. Exiting', scope)
-            GRACEFUL_STOP.set()
-    except Exception:
-        scope = False
+        raise exception.DatabaseException(
+            "Database was not updated, daemon won't start"
+        )
 
     threads = list()
-    for worker_number in range(0, total_workers):
-        kwargs = {'worker_number': worker_number,
-                  'total_workers': total_workers,
-                  'once': once,
-                  'sites': sites,
-                  'sleep_time': sleep_time,
-                  'account': account,
-                  'inputfile': inputfile,
-                  'set_metadata': set_metadata,
-                  'scope': scope,
-                  'dataset_lifetime': dataset_lifetime}
+    for _ in range(total_workers):
+        kwargs = {"once": once, "sleep_time": sleep_time, "inputfile": inputfile}
         threads.append(threading.Thread(target=automatix, kwargs=kwargs))
     [thread.start() for thread in threads]
     while threads[0].is_alive():
-        logging.log(logging.DEBUG, 'Still %i active threads', len(threads))
+        logging.log(logging.DEBUG, "Still %i active threads", len(threads))
         [thread.join(timeout=3.14) for thread in threads]
 
 
@@ -334,4 +278,4 @@ def stop(signum=None, frame=None):
     """
     Graceful exit.
     """
-    GRACEFUL_STOP.set()
+    graceful_stop.set()


Test patch:
diff --git a/lib/rucio/tests/test_automatix.py b/lib/rucio/tests/test_automatix.py
new file mode 100644
--- /dev/null
+++ b/lib/rucio/tests/test_automatix.py
@@ -0,0 +1,88 @@
+# -*- coding: utf-8 -*-
+# Copyright European Organization for Nuclear Research (CERN) since 2012
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+import os
+import tempfile
+
+import pytest
+
+from rucio.common.config import config_add_section, config_has_section, config_set, config_remove_option
+from rucio.common.utils import generate_uuid
+from rucio.common.types import InternalScope
+from rucio.core.did import list_dids, list_files
+from rucio.core.scope import add_scope
+from rucio.daemons.automatix.automatix import automatix
+from rucio.rse import rsemanager as rsemgr
+from rucio.tests.common import scope_name_generator
+
+
+@pytest.mark.noparallel(reason='changes global configuration value')
+def test_automatix(vo, root_account, rse_factory):
+    """Automatix: Test the automatix daemon"""
+    scope = scope_name_generator()
+    add_scope(scope=InternalScope(scope, vo), account=root_account)
+    if not config_has_section("automatix"):
+        config_add_section("automatix")
+    rse, rse_id = rse_factory.make_posix_rse()
+    config_set("automatix", "rses", rse)
+    config_set("automatix", "scope", scope)
+    if os.environ.get("POLICY") == "belleii":
+        config_set(
+            "automatix",
+            "dataset_pattern",
+            "did_prefix/version/project/date/campaign/release/datatype",
+        )
+        config_set("automatix", "file_pattern", "dsn/uuid")
+        config_set("automatix", "did_prefix", "/belle/ddm/tests")
+        config_set("automatix", "separator", "/")
+
+    project = generate_uuid()
+    project = project[:8]
+    test_dict = {
+        "type1": {
+            "probability": 100,
+            "nbfiles": 2,
+            "filesize": 1000000,
+            "metadata": {"project": project, "datatype": "AOD", "prod_step": "recon"},
+        }
+    }
+    with tempfile.NamedTemporaryFile("w") as file_:
+        json.dump(test_dict, file_)
+        file_.flush()
+        automatix(
+            inputfile=file_.name,
+            sleep_time=10,
+            once=True,
+        )
+        dids = [
+            did
+            for did in list_dids(
+                scope=InternalScope(scope, vo),
+                filters={"project": project},
+                did_type="collection",
+            )
+        ]
+        assert len(dids) == 1
+        rse_info = rsemgr.get_rse_info(rse, vo)
+        files = [file_ for file_ in list_files(InternalScope(scope, vo), dids[0])]
+        for file_ in files:
+            file_["scope"] = file_["scope"].external
+        status, file_dict = rsemgr.exists(rse_info, files)
+        assert status
+        for file_ in files:
+            assert file_dict["%s:%s" % (file_["scope"], file_["name"])] is True
+    config_remove_option("automatix", "rses")
+    config_remove_option("automatix", "scope")
diff --git a/lib/rucio/tests/test_multi_vo.py b/lib/rucio/tests/test_multi_vo.py
--- a/lib/rucio/tests/test_multi_vo.py
+++ b/lib/rucio/tests/test_multi_vo.py
@@ -47,7 +47,7 @@
 from rucio.client.scopeclient import ScopeClient
 from rucio.client.subscriptionclient import SubscriptionClient
 from rucio.client.uploadclient import UploadClient
-from rucio.common.config import config_get_bool, config_remove_option, config_set
+from rucio.common.config import config_get_bool, config_remove_option, config_set, config_has_section, config_add_section
 from rucio.common.exception import AccessDenied, Duplicate, InvalidRSEExpression, UnsupportedAccountName, \
     UnsupportedOperation, RucioException
 from rucio.common.types import InternalAccount, InternalScope
@@ -1089,12 +1089,23 @@ def test_automatix(self):
                                      'delete': 1}}}
         rse_client.add_rse(shr_rse)
         rse_client.add_rse_attribute(rse=shr_rse, key='verify_checksum', value=False)
+        rse_client.add_rse_attribute(rse=shr_rse, key='skip_upload_stat', value=True)
         rse_client.add_protocol(shr_rse, mock_protocol)
         add_rse(shr_rse, 'root', **self.new_vo)
         add_rse_attribute(rse=shr_rse, key='verify_checksum', value=False, issuer='root', **self.new_vo)
+        add_rse_attribute(rse=shr_rse, key='skip_upload_stat', value=True, issuer='root', **self.new_vo)
         add_protocol(rse=shr_rse, data=mock_protocol, issuer='root', **self.new_vo)
 
-        automatix(sites=[shr_rse], inputfile='/opt/rucio/etc/automatix.json', sleep_time=30, account='root', once=True, scope=shr_scope)
+        if not config_has_section("automatix"):
+            config_add_section("automatix")
+        config_set("automatix", "rses", shr_rse)
+        config_set("automatix", "scope", shr_scope)
+
+        automatix(
+            inputfile='/opt/rucio/etc/automatix.json',
+            sleep_time=10,
+            once=True,
+        )
 
         did_list_tst = list(DIDClient().list_dids(shr_scope, {}))
         did_list_new = list(list_dids(shr_scope, {}, **self.new_vo))
@@ -1107,6 +1118,9 @@ def test_automatix(self):
         assert len(replicas_tst) != 0
         assert len(replicas_new) == 0
 
+        config_remove_option("automatix", "rses")
+        config_remove_option("automatix", "scope")
+
 
 class TestVOMap(unittest.TestCase):
     """ Test VO Mapping functions. """