stifboy

fileconveyor - arbitrator.py

Mar 31st, 2014
345
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 59.24 KB | None | 0 0
  1. import logging
  2. import logging.handlers
  3. import Queue
  4. import os
  5. import stat
  6. import threading
  7. import time
  8. import sys
  9. import sqlite3
  10. from UserList import UserList
  11. import os.path
  12. import signal
  13.  
  14.  
  15. FILE_CONVEYOR_PATH = os.path.abspath(os.path.dirname(__file__))
  16.  
  17.  
  18. # HACK to make sure that Django-related libraries can be loaded: include dummy
  19. # settings if necessary.
  20. if not 'DJANGO_SETTINGS_MODULE' in os.environ:
  21.     os.environ['DJANGO_SETTINGS_MODULE'] = 'django_settings'
  22.  
  23.  
  24. from settings import *
  25. from config import *
  26. from persistent_queue import *
  27. from persistent_list import *
  28. from fsmonitor import *
  29. from filter import *
  30. from processors.processor import *
  31. from transporters.transporter import Transporter, ConnectionError
  32. from daemon_thread_runner import *
  33.  
  34.  
  35. # Copied from django.utils.functional
  36. def curry(_curried_func, *args, **kwargs):
  37.     def _curried(*moreargs, **morekwargs):
  38.         return _curried_func(*(args+moreargs), **dict(kwargs, **morekwargs))
  39.     return _curried
  40.  
  41.  
  42. class AdvancedQueue(UserList):
  43.     """queue that supports peeking and jumping"""
  44.  
  45.     def peek(self):
  46.         return self[0]
  47.  
  48.     def jump(self, item):
  49.         self.insert(0, item)
  50.  
  51.     def put(self, item):
  52.         self.append(item)
  53.  
  54.     def get(self):
  55.         return self.pop(0)
  56.  
  57.     def qsize(self):
  58.         return len(self)
  59.  
  60.  
  61. # Define exceptions.
  62. class ArbitratorError(Exception): pass
  63. class ArbitratorInitError(ArbitratorError): pass
  64. class ConfigError(ArbitratorInitError): pass
  65. class ProcessorAvailabilityTestError(ArbitratorInitError): pass
  66. class TransporterAvailabilityTestError(ArbitratorInitError): pass
  67. class ServerConnectionTestError(ArbitratorInitError): pass
  68. class FSMonitorInitError(ArbitratorInitError): pass
  69.  
  70.  
  71. class Arbitrator(threading.Thread):
  72.     """docstring for arbitrator"""
  73.  
  74.  
  75.     DELETE_OLD_FILE = 0xFFFFFFFF
  76.     PROCESSED_FOR_ANY_SERVER = None
  77.  
  78.  
  79.     def __init__(self, configfile="config.xml", restart=False):
  80.         threading.Thread.__init__(self, name="ArbitratorThread")
  81.         self.lock = threading.Lock()
  82.         self.die = False
  83.         self.processorchains_running = 0
  84.         self.transporters_running = 0
  85.         self.last_retry = 0
  86.  
  87.         # Set up logger.
  88.         self.logger = logging.getLogger("Arbitrator")
  89.         self.logger.setLevel(FILE_LOGGER_LEVEL)
  90.         # Handlers.
  91.         fileHandler = logging.handlers.RotatingFileHandler(LOG_FILE, maxBytes=5242880, backupCount=5)
  92.         consoleHandler = logging.StreamHandler()
  93.         consoleHandler.setLevel(CONSOLE_LOGGER_LEVEL)
  94.         # Formatters.
  95.         formatter = logging.Formatter("%(asctime)s - %(name)-25s - %(levelname)-8s - %(message)s")
  96.         fileHandler.setFormatter(formatter)
  97.         consoleHandler.setFormatter(formatter)
  98.         self.logger.addHandler(fileHandler)
  99.         self.logger.addHandler(consoleHandler)
  100.         if restart:
  101.             self.logger.warning("File Conveyor has restarted itself!")
  102.         self.logger.warning("File Conveyor is initializing.")
  103.  
  104.         # Load config file.
  105.         self.configfile = configfile
  106.         self.logger.info("Loading config file.")
  107.         self.config = Config("Arbitrator")
  108.         self.config_errors = self.config.load(self.configfile)
  109.         self.logger.warning("Loaded config file.")
  110.         if self.config_errors > 0:
  111.             self.logger.error("Cannot continue, please fix the errors in the config file first.")
  112.             raise ConfigError("Consult the log file for details.")
  113.  
  114.         # TRICKY: set the "symlinkWithin" setting for "symlink_or_copy"
  115.         # transporters First calculate the value for the "symlinkWithin"
  116.         # setting.
  117.         source_paths = []
  118.         for source in self.config.sources.values():
  119.             source_paths.append(source["scan_path"])
  120.         symlinkWithin = ":".join(source_paths)
  121.         # Then set it for every server that uses this transporter.
  122.         for name in self.config.servers.keys():
  123.             if self.config.servers[name]["transporter"] == "symlink_or_copy":
  124.                 self.config.servers[name]["settings"]["symlinkWithin"] = symlinkWithin
  125.  
  126.         # Verify that all referenced processors are available.
  127.         processors_not_found = 0
  128.         for source in self.config.rules.keys():
  129.             for rule in self.config.rules[source]:
  130.                 if not rule["processorChain"] is None:
  131.                     for processor in rule["processorChain"]:
  132.                         processor_class = self._import_processor(processor)
  133.                         if not processor_class:
  134.                             processors_not_found += 1
  135.         if processors_not_found > 0:
  136.             raise ProcessorAvailabilityTestError("Consult the log file for details")
  137.  
  138.         # Verify that all referenced transporters are available.
  139.         transporters_not_found = 0
  140.         for server in self.config.servers.keys():
  141.             transporter_name = self.config.servers[server]["transporter"]
  142.             transporter_class = self._import_transporter(transporter_name)
  143.             if not transporter_class:
  144.                 transporters_not_found += 1
  145.         if transporters_not_found > 0:
  146.             raise TransporterAvailabilityTestError("Consult the log file for details")
  147.  
  148.         # Verify that each of the servers works.
  149.         successful_server_connections = 0
  150.         for server in self.config.servers.keys():
  151.             transporter = self.__create_transporter(server)
  152.             if transporter:
  153.                 successful_server_connections += 1
  154.             del transporter
  155.         failed_server_connections = len(self.config.servers) - successful_server_connections
  156.         if failed_server_connections > 0:
  157.             self.logger.error("Server connection tests: could not connect with %d servers." % (failed_server_connections))
  158.             raise ServerConnectionTestError("Consult the log file for details.")
  159.         else:
  160.             self.logger.warning("Server connection tests succesful!")
  161.  
  162.  
  163.     def __setup(self):
  164.         self.processor_chain_factory = ProcessorChainFactory("Arbitrator", WORKING_DIR)
  165.  
  166.         # Create transporter (cfr. worker thread) pools for each server.
  167.         # Create one initial transporter per pool, possible other transporters
  168.         # will be created on-demand.
  169.         self.transporters = {}
  170.         for server in self.config.servers.keys():
  171.             self.transporters[server] = []
  172.             self.logger.warning("Setup: created transporter pool for the '%s' server." % (server))
  173.  
  174.         # Collecting all necessary metadata for each rule.
  175.         self.rules = []
  176.         for source in self.config.sources.values():
  177.             # Create a function to prepend the source's scan path to another
  178.             # path.
  179.             prepend_scan_path = lambda path: os.path.join(source["scan_path"], path)
  180.             if self.config.rules.has_key(source["name"]):
  181.                 for rule in self.config.rules[source["name"]]:
  182.                     if rule["filterConditions"] is None:
  183.                         filter = None
  184.                     else:
  185.                         if rule["filterConditions"].has_key("paths"):
  186.                             # Prepend the source's scan path (effectively the
  187.                             # "root path") for a rule to each of the paths in
  188.                             # the "paths" condition in the filter.
  189.                             paths = map(prepend_scan_path, rule["filterConditions"]["paths"].split(":"))
  190.                             rule["filterConditions"]["paths"] = ":".join(paths)
  191.                         filter = Filter(rule["filterConditions"])
  192.  
  193.                     # Store all the rule metadata.
  194.                     self.rules.append({
  195.                         "source"         : source["name"],
  196.                         "label"          : rule["label"],
  197.                         "filter"         : filter,
  198.                         "processorChain" : rule["processorChain"],
  199.                         "destinations"   : rule["destinations"],
  200.                         "deletionDelay"  : rule["deletionDelay"],
  201.                     })
  202.                     self.logger.info("Setup: collected all metadata for rule '%s' (source: '%s')." % (rule["label"], source["name"]))
  203.  
  204.         # Initialize the the persistent 'pipeline' queue, the persistent
  205.         # 'files in pipeline' and 'failed files' lists and the 'discover',
  206.         # 'filter', 'process', 'transport', 'db' and 'retry' queues. Finally,
  207.         # initialize the 'remaining transporters' dictionary of lists.
  208.         self.pipeline_queue = PersistentQueue("pipeline_queue", PERSISTENT_DATA_DB)
  209.         self.logger.warning("Setup: initialized 'pipeline' persistent queue, contains %d items." % (self.pipeline_queue.qsize()))
  210.         self.files_in_pipeline =  PersistentList("pipeline_list", PERSISTENT_DATA_DB)
  211.         num_files_in_pipeline = len(self.files_in_pipeline)
  212.         self.logger.warning("Setup: initialized 'files_in_pipeline' persistent list, contains %d items." % (num_files_in_pipeline))
  213.         self.failed_files = PersistentList("failed_files_list", PERSISTENT_DATA_DB)
  214.         num_failed_files = len(self.failed_files)
  215.         self.logger.warning("Setup: initialized 'failed_files' persistent list, contains %d items." % (num_failed_files))
  216.         self.files_to_delete = PersistentList("files_to_delete_list", PERSISTENT_DATA_DB)
  217.         num_files_to_delete = len(self.files_to_delete)
  218.         self.logger.warning("Setup: initialized 'files_to_delete' persistent list, contains %d items." % (num_files_to_delete))
  219.         self.discover_queue  = Queue.Queue()
  220.         self.filter_queue    = Queue.Queue()
  221.         self.process_queue   = Queue.Queue()
  222.         self.transport_queue = {}
  223.         for server in self.config.servers.keys():
  224.             self.transport_queue[server] = AdvancedQueue()
  225.         self.db_queue        = Queue.Queue()
  226.         self.retry_queue     = Queue.Queue()
  227.         self.remaining_transporters = {}
  228.  
  229.         # Move files from the 'files_in_pipeline' persistent list to the
  230.         # pipeline queue. This is what prevents files from being dropped from
  231.         # the pipeline!
  232.         pipelined_items = []
  233.         for item in self.files_in_pipeline:
  234.             pipelined_items.append(item)
  235.             self.pipeline_queue.put(item)
  236.         for item in pipelined_items:
  237.             self.files_in_pipeline.remove(item)
  238.         self.logger.warning("Setup: moved %d items from the 'files_in_pipeline' persistent list into the 'pipeline' persistent queue." % (num_files_in_pipeline))
  239.  
  240.         # Move files from the 'failed_files' persistent list to the
  241.         # pipeline queue. This is what ensures that even problematic files
  242.         # are not forgotten!
  243.         self.__allow_retry()
  244.  
  245.         # Create connection to synced files DB.
  246.         self.dbcon = sqlite3.connect(SYNCED_FILES_DB)
  247.         self.dbcon.text_factory = unicode # This is the default, but we set it explicitly, just to be sure.
  248.         self.dbcur = self.dbcon.cursor()
  249.         self.dbcur.execute("CREATE TABLE IF NOT EXISTS synced_files(input_file text, transported_file_basename text, url text, server text)")
  250.         self.dbcur.execute("CREATE UNIQUE INDEX IF NOT EXISTS file_unique_per_server ON synced_files (input_file, server)")
  251.         self.dbcon.commit()
  252.         self.dbcur.execute("SELECT COUNT(input_file) FROM synced_files")
  253.         num_synced_files = self.dbcur.fetchone()[0]
  254.         self.logger.warning("Setup: connected to the synced files DB. Contains metadata for %d previously synced files." % (num_synced_files))
  255.  
  256.         # Initialize the FSMonitor.
  257.         fsmonitor_class = get_fsmonitor()
  258.         self.fsmonitor = fsmonitor_class(self.fsmonitor_callback, True, True, self.config.ignored_dirs.split(":"), "fsmonitor.db", "Arbitrator")
  259.         self.logger.warning("Setup: initialized FSMonitor.")
  260.  
  261.         # Monitor all sources' scan paths.
  262.         for source in self.config.sources.values():
  263.             self.logger.info("Setup: monitoring '%s' (%s)." % (source["scan_path"], source["name"]))
  264.             self.fsmonitor.add_dir(source["scan_path"], FSMonitor.CREATED | FSMonitor.MODIFIED | FSMonitor.DELETED)
  265.  
  266.  
  267.     def run(self):
  268.         if self.config_errors > 0:
  269.             return
  270.  
  271.         # Do all setup within the run() method to ensure all thread-bound
  272.         # objects are created in the right thread.
  273.         self.__setup()
  274.  
  275.         self.fsmonitor.start()
  276.  
  277.         self.clean_up_working_dir()
  278.  
  279.         self.logger.warning("Fully up and running now.")
  280.         try:
  281.             while not self.die:
  282.                 self.__process_discover_queue()
  283.                 self.__process_pipeline_queue()
  284.                 self.__process_filter_queue()
  285.                 self.__process_process_queue()
  286.                 self.__process_transport_queues()
  287.                 self.__process_db_queue()
  288.                 self.__process_files_to_delete()
  289.                 self.__process_retry_queue()
  290.                 self.__allow_retry()
  291.  
  292.                 # Processing the queues 5 times per second is more than sufficient
  293.                 # because files are modified, processed and transported much
  294.                 # slower than that.
  295.                 time.sleep(0.2)
  296.         except Exception, e:
  297.             self.logger.exception("Unhandled exception of type '%s' detected, arguments: '%s'." % (e.__class__.__name__, e.args))
  298.             self.logger.error("Stopping File Conveyor to ensure the application is stopped in a clean manner.")
  299.             os.kill(os.getpid(), signal.SIGTERM)
  300.         self.logger.warning("Stopping.")
  301.  
  302.         # Stop the FSMonitor and wait for its thread to end.
  303.         self.fsmonitor.stop()
  304.         self.fsmonitor.join()
  305.         self.logger.warning("Stopped FSMonitor.")
  306.  
  307.         # Sync the discover queue one more time: now that the FSMonitor has
  308.         # been stopped, no more new discoveries will be made and we can safely
  309.         # sync the last batch of discovered files.
  310.         self.__process_discover_queue()
  311.         self.logger.info("Final sync of discover queue to pipeline queue made.")
  312.  
  313.         # Stop the transporters and wait for their threads to end.
  314.         for server in self.transporters.keys():
  315.             if len(self.transporters[server]):
  316.                 for transporter in self.transporters[server]:
  317.                     transporter.stop()
  318.                     transporter.join()
  319.                 self.logger.warning("Stopped transporters for the '%s' server." % (server))
  320.  
  321.         # Log information about the persistent data.
  322.         self.logger.warning("'pipeline' persistent queue contains %d items." % (self.pipeline_queue.qsize()))
  323.         self.logger.warning("'files_in_pipeline' persistent list contains %d items." % (len(self.files_in_pipeline)))
  324.         self.logger.warning("'failed_files' persistent list contains %d items." % (len(self.failed_files)))
  325.         self.logger.warning("'files_to_delete' persistent list contains %d items." % (len(self.files_to_delete)))
  326.  
  327.         # Log information about the synced files DB.
  328.         self.dbcur.execute("SELECT COUNT(input_file) FROM synced_files")
  329.         num_synced_files = self.dbcur.fetchone()[0]
  330.         self.logger.warning("synced files DB contains metadata for %d synced files." % (num_synced_files))
  331.  
  332.         # Clean up working directory.
  333.         self.clean_up_working_dir()
  334.  
  335.         # Final message, then remove all loggers.
  336.         self.logger.warning("File Conveyor has shut down.")
  337.         while len(self.logger.handlers):
  338.             self.logger.removeHandler(self.logger.handlers[0])
  339.         logging.shutdown()
  340.  
  341.  
  342.     def __process_discover_queue(self):
  343.         # No QUEUE_PROCESS_BATCH_SIZE limitation here because the data must
  344.         # be moved to a persistent datastructure ASAP.
  345.  
  346.         self.lock.acquire()
  347.         while self.discover_queue.qsize() > 0:
  348.  
  349.             # Discover queue -> pipeline queue.
  350.             (input_file, event) = self.discover_queue.get()
  351.             item = self.pipeline_queue.get_item_for_key(key=input_file)
  352.             # If the file does not yet exist in the pipeline queue, put() it.
  353.             if item is None:
  354.                 self.pipeline_queue.put(item=(input_file, event), key=input_file)
  355.             # Otherwise, merge the events, to prevent unnecessary actions.
  356.             # See https://github.com/wimleers/fileconveyor/issues/68.
  357.             else:
  358.                 old_event = item[1]
  359.                 merged_event = FSMonitor.MERGE_EVENTS[old_event][event]
  360.                 if merged_event is not None:
  361.                     self.pipeline_queue.update(item=(input_file, merged_event), key=input_file)
  362.                     self.logger.info("Pipeline queue: merged events for '%s': %s + %s = %s." % (input_file, FSMonitor.EVENTNAMES[old_event], FSMonitor.EVENTNAMES[event], FSMonitor.EVENTNAMES[merged_event]))
  363.                 # The events being merged cancel each other out, thus remove
  364.                 # the file from the pipeline queue.
  365.                 else:
  366.                     self.pipeline_queue.remove_item_for_key(key=input_file)
  367.                     self.logger.info("Pipeline queue: merged events for '%s': %s + %s cancel each other out, thus removed this file." % (input_file, FSMonitor.EVENTNAMES[old_event], FSMonitor.EVENTNAMES[event]))
  368.             self.logger.info("Discover queue -> pipeline queue: '%s'." % (input_file))
  369.         self.lock.release()
  370.  
  371.  
  372.     def __process_pipeline_queue(self):
  373.         processed = 0
  374.  
  375.         # As soon as there's room in the pipeline, move the file from the
  376.         # pipeline queue into the pipeline.
  377.         while processed < QUEUE_PROCESS_BATCH_SIZE and self.pipeline_queue.qsize() > 0 and len(self.files_in_pipeline) < MAX_FILES_IN_PIPELINE:
  378.             self.lock.acquire()
  379.  
  380.             # Peek the first item from the pipeline queue and store it in the
  381.             # persistent 'files_in_pipeline' list. By peeking instead of
  382.             # getting, ththe data can never get lost.
  383.             self.files_in_pipeline.append(self.pipeline_queue.peek())
  384.  
  385.             # Pipeline queue -> filter queue.
  386.             (input_file, event) = self.pipeline_queue.get()
  387.             self.filter_queue.put((input_file, event))
  388.  
  389.             self.lock.release()
  390.             self.logger.info("Pipeline queue -> filter queue: '%s'." % (input_file))
  391.             processed += 1
  392.  
  393.  
  394.     def __process_filter_queue(self):
  395.         processed = 0
  396.  
  397.         while processed < QUEUE_PROCESS_BATCH_SIZE and self.filter_queue.qsize() > 0:
  398.             # Filter queue -> process/transport queue.
  399.             self.lock.acquire()
  400.             (input_file, event) = self.filter_queue.get()
  401.             self.lock.release()
  402.  
  403.             # The file may have already been deleted, e.g. when the file was
  404.             # moved from the pipeline list into the pipeline queue after the
  405.             # application was interrupted. When that's the case, drop the
  406.             # file from the pipeline.
  407.             touched = event == FSMonitor.CREATED or event == FSMonitor.MODIFIED
  408.             if touched and not os.path.exists(input_file):
  409.                 self.lock.acquire()
  410.                 self.files_in_pipeline.remove((input_file, event))
  411.                 self.lock.release()
  412.                 self.logger.info("Filtering: dropped '%s' because it no longer exists." % (input_file))
  413.                 continue
  414.  
  415.             # Find all rules that apply to the detected file event.
  416.             match_found = False
  417.             file_is_deleted = event == FSMonitor.DELETED
  418.             current_time = time.time()
  419.  
  420.             for rule in self.rules:
  421.                 # Try to find a rule that matches the file.
  422.                 if input_file.startswith(self.config.sources[rule["source"]]["scan_path"]) and \
  423.                    (rule["filter"] is None
  424.                    or
  425.                    rule["filter"].matches(input_file, file_is_deleted=file_is_deleted)):
  426.                     match_found = True
  427.                     self.logger.info("Filtering: '%s' matches the '%s' rule for the '%s' source!" % (input_file, rule["label"], rule["source"]))
  428.  
  429.                     # If the file was deleted, and the rule that matches this
  430.                     # file has a deletionDelay configured, then don't sync
  431.                     # this file deletion: it was performed by File Conveyor.
  432.                     # *Except* when the file is still scheduled for deletion:
  433.                     # that means the file could not have been deleted by File
  434.                     # Conveyor and hence the deletion should be synced.
  435.                     if event == FSMonitor.DELETED and rule["deletionDelay"] is not None:
  436.                         file_still_scheduled_for_deletion = False
  437.                         scheduled_deletion_time = None
  438.                         self.lock.acquire()
  439.                         for (file_to_delete, deletion_time) in self.files_to_delete:
  440.                             if input_file == file_to_delete:
  441.                                 file_still_scheduled_for_deletion = True
  442.                                 scheduled_deletion_time = deletion_time
  443.                                 break
  444.                         self.lock.release()
  445.  
  446.                         # Unschedule deletion.
  447.                         if file_still_scheduled_for_deletion:
  448.                             self.lock.acquire()
  449.                             self.files_to_delete.remove((input_file, scheduled_deletion_time))
  450.                             self.logger.warning("Unscheduled '%s' for deletion." % (input_file))
  451.                             self.lock.release()
  452.                         else:
  453.                         # A deletion by File Conveyor: don't sync this deletion.
  454.                             break
  455.  
  456.                     # If the file was deleted, also delete the file on all
  457.                     # servers.
  458.                     self.lock.acquire()
  459.                     servers = rule["destinations"].keys()
  460.                     self.remaining_transporters[input_file + str(event) + repr(rule)] = servers
  461.                     if event == FSMonitor.DELETED:
  462.                         # Look up the transported file's base name. This might
  463.                         # be different from the input file's base name due to
  464.                         # processing.
  465.                         self.dbcur.execute("SELECT transported_file_basename FROM synced_files WHERE input_file=?", (input_file, ))
  466.                         result = self.dbcur.fetchone()
  467.  
  468.                     if event == FSMonitor.DELETED and not result is None:
  469.                         transport_file_basename = result[0]
  470.                         # The output file that should be transported doesn't
  471.                         # exist anymore, because it was deleted. So we create
  472.                         # a filename that is the same as the original, except
  473.                         # with the different base name.
  474.                         fake_output_file = os.path.join(os.path.dirname(input_file), transport_file_basename)
  475.                         # Queue the transport (deletion).
  476.                         for server in servers:
  477.                             self.transport_queue[server].put((input_file, event, rule, Arbitrator.PROCESSED_FOR_ANY_SERVER, fake_output_file))
  478.                             self.logger.info("Filtering: queued transporter to server '%s' for file '%s' to delete it ('%s' rule)." % (server, input_file, rule["label"]))
  479.                     else:
  480.                         # If a processor chain is configured, queue the file
  481.                         # to be processed. Otherwise, immediately queue the
  482.                         # file to be transported
  483.                         if not rule["processorChain"] is None:
  484.                             # Check if there is at least one processor that
  485.                             # will create output that is different per server.
  486.                             per_server = False
  487.                             for processor_classname in rule["processorChain"]:
  488.                                 # Get a reference to this processor class.
  489.                                 processor_class = self._import_processor(processor_classname)
  490.                                 if getattr(processor_class, 'different_per_server', False) == True:
  491.                                     # This processor would create different
  492.                                     # output per server, but will it also
  493.                                     # process this file?
  494.                                     if processor_class.would_process_input_file(input_file):
  495.                                         per_server = True
  496.                                         break
  497.  
  498.                             if per_server:
  499.                                 for server in servers:
  500.                                     # If the event for the file is creation
  501.                                     # and the file has been synced to this
  502.                                     # server already, don't process it again
  503.                                     # (which will lead to it being resynced
  504.                                     # and reinserted into the database, which
  505.                                     # will cause a IntegrityError).
  506.                                     if event == FSMonitor.CREATED:
  507.                                         self.dbcur.execute("SELECT COUNT(*) FROM synced_files WHERE input_file=? AND server=?", (input_file, server))
  508.                                         file_is_synced = self.dbcur.fetchone()[0] == 1
  509.                                     if event == FSMonitor.CREATED and file_is_synced:
  510.                                         self.logger.info("Filtering: not processing '%s' for server '%s', because it has been synced already to this server (rule: '%s')." % (input_file, server, rule["label"]))
  511.                                         self.remaining_transporters[input_file + str(event) + repr(rule)].remove(server)
  512.                                     else:
  513.                                         self.process_queue.put((input_file, event, rule, server))
  514.                                         self.logger.info("Filter queue -> process queue: '%s' for server '%s' (rule: '%s')." % (input_file, server, rule["label"]))
  515.                             else:
  516.                                 self.process_queue.put((input_file, event, rule, Arbitrator.PROCESSED_FOR_ANY_SERVER))
  517.                                 self.logger.info("Filter queue -> process queue: '%s' (rule: '%s')." % (input_file, rule["label"]))
  518.                         else:
  519.                             output_file = input_file
  520.                             for server in servers:
  521.                                 self.transport_queue[server].put((input_file, event, rule, Arbitrator.PROCESSED_FOR_ANY_SERVER, output_file))
  522.                                 self.logger.info("Filter queue -> transport queue: '%s' (rule: '%s')." % (input_file, rule["label"]))
  523.                     self.lock.release()
  524.  
  525.             # Log the lack of matches.
  526.             if not match_found:
  527.                 self.lock.acquire()
  528.                 self.files_in_pipeline.remove((input_file, event))
  529.                 self.lock.release()
  530.                 self.logger.info("Filter queue: dropped '%s' because it doesn't match any rules." % (input_file))
  531.  
  532.             processed += 1
  533.  
  534.  
  535.     def __process_process_queue(self):
  536.         processed = 0
  537.  
  538.         while processed< QUEUE_PROCESS_BATCH_SIZE and self.process_queue.qsize() > 0 and self.processorchains_running < MAX_SIMULTANEOUS_PROCESSORCHAINS:
  539.             # Process queue -> ProcessorChain -> processor_chain_callback -> transport/db queue.
  540.             self.lock.acquire()
  541.             (input_file, event, rule, processed_for_server) = self.process_queue.get()
  542.             self.lock.release()
  543.  
  544.             # Create curried callbacks so we can pass additional data to the
  545.             # processor chain callback without passing it to the processor
  546.             # chain itself (which cannot handle sending additional data to its
  547.             # callback functions).
  548.             curried_callback = curry(self.processor_chain_callback,
  549.                                      event=event,
  550.                                      rule=rule,
  551.                                      processed_for_server=processed_for_server
  552.                                      )
  553.             curried_error_callback = curry(self.processor_chain_error_callback,
  554.                                            event=event
  555.                                            )
  556.  
  557.             # Start the processor chain.
  558.             document_root = None
  559.             base_path     = None
  560.             if self.config.sources[rule["source"]].has_key("document_root"):
  561.                 document_root = self.config.sources[rule["source"]]["document_root"]
  562.             if self.config.sources[rule["source"]].has_key("base_path"):
  563.                 base_path = self.config.sources[rule["source"]]["base_path"]
  564.             processor_chain = self.processor_chain_factory.make_chain_for(input_file,
  565.                                                                           rule["processorChain"],
  566.                                                                           document_root,
  567.                                                                           base_path,
  568.                                                                           processed_for_server,
  569.                                                                           curried_callback,
  570.                                                                           curried_error_callback
  571.                                                                           )
  572.             processor_chain.start()
  573.             self.processorchains_running += 1
  574.  
  575.             # Log.
  576.             processor_chain_string = "->".join(rule["processorChain"])
  577.             if processed_for_server == Arbitrator.PROCESSED_FOR_ANY_SERVER:
  578.                 self.logger.debug("Process queue: started the '%s' processor chain for the file '%s'." % (processor_chain_string, input_file))
  579.             else:
  580.                 self.logger.debug("Process queue: started the '%s' processor chain for the file '%s' for the server '%s'." % (processor_chain_string, input_file, processed_for_server))
  581.             processed += 1
  582.  
  583.  
  584.     def __process_transport_queues(self):
  585.         for server in self.config.servers.keys():
  586.             processed = 0
  587.  
  588.             while processed < QUEUE_PROCESS_BATCH_SIZE and self.transport_queue[server].qsize() > 0:
  589.                 # Peek at the first item from the queue. We cannot get the
  590.                 # item from the queue, because there may be no transporter
  591.                 # available, in which case the file should remain queued.
  592.                 self.lock.acquire()
  593.                 (input_file, event, rule, processed_for_server, output_file) = self.transport_queue[server].peek()
  594.                 self.lock.release()
  595.  
  596.                 # Derive the action from the event.
  597.                 if event == FSMonitor.DELETED:
  598.                     action = Transporter.DELETE
  599.                 elif event == FSMonitor.CREATED or event == FSMonitor.MODIFIED:
  600.                     action = Transporter.ADD_MODIFY
  601.                 elif event == Arbitrator.DELETE_OLD_FILE:
  602.                     # TRICKY: if the event is neither of DELETED, CREATED, nor
  603.                     # MODIFIED, which everywhere else in the arbitrator it
  604.                     # should be, then it must be the special case of a file
  605.                     # that has been modified and already transported, but the
  606.                     # old file must still be deleted. Hence we map this event
  607.                     # to the Transporter's DELETE action.
  608.                     action = Transporter.DELETE
  609.                 else:
  610.                     raise Exception("Non-existing event set.")
  611.  
  612.                 # Get the additional settings from the rule.
  613.                 dst_parent_path = ""
  614.                 if rule["destinations"][server].has_key("path"):
  615.                     dst_parent_path = rule["destinations"][server]["path"]
  616.  
  617.                 (id, place_in_queue, transporter) = self.__get_transporter(server)
  618.                 if not transporter is None:
  619.                     # A transporter is available!
  620.                     # Transport queue -> Transporter -> transporter_callback -> db queue.
  621.                     self.lock.acquire()
  622.                     (input_file, event, rule, processed_for_server, output_file) = self.transport_queue[server].get()
  623.                     self.lock.release()
  624.  
  625.                     # Create curried callbacks so we can pass additional data
  626.                     # to the transporter callback without passing it to the
  627.                     # transporter itself (which cannot handle sending
  628.                     # additional data to its callback functions).
  629.                     curried_callback = curry(self.transporter_callback,
  630.                                              input_file=input_file,
  631.                                              event=event,
  632.                                              rule=rule,
  633.                                              processed_for_server=processed_for_server,
  634.                                              server=server
  635.                                              )
  636.                     curried_error_callback = curry(self.transporter_error_callback,
  637.                                                    input_file=input_file,
  638.                                                    event=event
  639.                                                    )
  640.  
  641.  
  642.                     # Calculate src and dst for the file.
  643.                     # - The src is the output file of the processor.
  644.                     # - The dst is the output file, but its source parent path
  645.                     #   (the working directory or its source root path) must
  646.                     #   be stripped and the destination parent path must be
  647.                     #   prepended.
  648.                     #   e.g.:
  649.                     #     - src                         -> dst
  650.                     #     - /htdocs/mysite/dir/the_file -> dir/the_file
  651.                     #     - /tmp/dir/the_file           -> dir/the_file
  652.                     src = output_file
  653.                     relative_paths = [WORKING_DIR, self.config.sources[rule["source"]]["scan_path"]]
  654.                     dst = self.__calculate_transporter_dst(output_file, dst_parent_path, relative_paths)
  655.  
  656.                     # Start the transport.
  657.                     transporter.sync_file(src, dst, action, curried_callback, curried_error_callback)
  658.  
  659.                     self.logger.info("Transport queue: '%s' to transfer to server '%s' with transporter #%d (of %d), place %d in the queue." % (output_file, server, id + 1, len(self.transporters[server]), place_in_queue))
  660.                 else:
  661.                     self.logger.debug("Transporting: no more transporters are available for server '%s'." % (server))
  662.                     break
  663.  
  664.                 processed += 1
  665.  
  666.  
  667.     def __process_db_queue(self):
  668.         processed = 0
  669.  
  670.         while processed < QUEUE_PROCESS_BATCH_SIZE and self.db_queue.qsize() > 0:
  671.             # DB queue -> database.
  672.             self.lock.acquire()
  673.             (input_file, event, rule, processed_for_server, output_file, transported_file, url, server) = self.db_queue.get()
  674.             self.lock.release()
  675.  
  676.             # Commit the result to the database.            
  677.             remove_server_from_remaining_transporters = True
  678.             transported_file_basename = os.path.basename(output_file)
  679.             if event == FSMonitor.CREATED:
  680.                 try:
  681.                     self.dbcur.execute("INSERT INTO synced_files VALUES(?, ?, ?, ?)", (input_file, transported_file_basename, url, server))
  682.                     self.dbcon.commit()
  683.                 except sqlite3.IntegrityError, e:
  684.                     self.logger.critical("Database integrity error: %s. Duplicate key: input_file = '%s', server = '%s'." % (e, input_file, server))
  685.             elif event == FSMonitor.MODIFIED:
  686.                 self.dbcur.execute("SELECT COUNT(*) FROM synced_files WHERE input_file=? AND server=?", (input_file, server))
  687.                 if self.dbcur.fetchone()[0] > 0:
  688.  
  689.                     # Look up the transported file's base name. This
  690.                     # might be different from the input file's base
  691.                     # name due to processing.
  692.                     self.dbcur.execute("SELECT transported_file_basename FROM synced_files WHERE input_file=? AND server=?", (input_file, server))
  693.                     old_transport_file_basename = self.dbcur.fetchone()[0]
  694.  
  695.                     # Update the transported_file_basename and url fields for
  696.                     # the input_file that has been transported.
  697.                     self.dbcur.execute("UPDATE synced_files SET transported_file_basename=?, url=? WHERE input_file=? AND server=?", (transported_file_basename, url, input_file, server))
  698.                     self.dbcon.commit()
  699.                    
  700.                     # If a file was modified that had already been synced
  701.                     # before and now has a different basename for the
  702.                     # transported file than before, we first have to delete
  703.                     # the old transported file before all work is done.
  704.                     # remove_server_from_remaining_transporters is set to
  705.                     # False for this case.
  706.                     if old_transport_file_basename != transported_file_basename:
  707.                         remove_server_from_remaining_transporters = False
  708.  
  709.                         # The output file that should be transported only
  710.                         # exists on the server. So we create a filename that
  711.                         # is the same as the old transported file.
  712.                         fake_output_file = os.path.join(os.path.dirname(input_file), old_transport_file_basename)
  713.                         # Change the event to Arbitrator.DELETE_OLD_FILE,
  714.                         # which __process_transport_queues() will recognize
  715.                         # and perform a deletion for. After the transporter
  716.                         # callback gets called, this pseudo-event will end up
  717.                         # in __process_db_queue() (this method) once again and
  718.                         # will change the event back the original,
  719.                         # FSMonitor.MODIFIED, so we can remove it from the
  720.                         # 'files_in_pipeline' persistent list.
  721.                         pseudo_event = Arbitrator.DELETE_OLD_FILE
  722.                         # Queue the transport (deletion), but jump the queue!.
  723.                         self.transport_queue[server].jump((input_file, pseudo_event, rule, Arbitrator.PROCESSED_FOR_ANY_SERVER, fake_output_file))
  724.                         self.logger.info("DB queue -> transport queue (jumped): '%s' to delete its old transported file '%s' on server '%s'." % (input_file, old_transport_file_basename, server))
  725.                 else:
  726.                     self.dbcur.execute("INSERT INTO synced_files VALUES(?, ?, ?, ?)", (input_file, transported_file_basename, url, server))
  727.                     self.dbcon.commit()
  728.             elif event == FSMonitor.DELETED:
  729.                 self.dbcur.execute("DELETE FROM synced_files WHERE input_file=? AND server=?", (input_file, server))
  730.                 self.dbcon.commit()
  731.             elif event == Arbitrator.DELETE_OLD_FILE:
  732.                 # This is a pseudo-event. See the comments for the
  733.                 # FSMonitor.MODIFIED-branch for details.
  734.                 event = FSMonitor.MODIFIED
  735.             else:
  736.                 raise Exception("Non-existing event set.")
  737.  
  738.             self.logger.debug("DB queue -> 'synced files' DB: '%s' (URL: '%s')." % (input_file, url))
  739.  
  740.             key = input_file + str(event) + repr(rule)
  741.  
  742.             # Remove this server from the 'remaining transporters' list for
  743.             # this input file/event/rule.
  744.             if remove_server_from_remaining_transporters:
  745.                 # TRICKY: This is wrapped in a try/except block because it's
  746.                 # possible that for example multiple "CREATED" and "DELETED"
  747.                 # events on the same file have been logged, which are then
  748.                 # potentially being synced at the same time. It is then
  749.                 # possible that one instance of the file syncing process has
  750.                 # already synced to one server and another instance has done
  751.                 # the same, but later. Because the key here is not universally
  752.                 # unique, but merely on the input file, event and rule,
  753.                 # collisions are possible.
  754.                 # Yes, this is a design flaw in the current version. It cannot
  755.                 # cause any problems though, merely duplicate work in highly
  756.                 # active environments.
  757.                 try:
  758.                     self.remaining_transporters[key].remove(server)
  759.                 except ValueError, e:
  760.                     pass
  761.  
  762.             # Only remove the file from the pipeline if no transporters are
  763.             # remaining.
  764.             if len(self.remaining_transporters[key]) == 0:
  765.                 # Delete the output file, but only if it's different from the
  766.                 # input file.
  767.                 touched = event == FSMonitor.CREATED or event == FSMonitor.MODIFIED
  768.                 if touched and not input_file == output_file and os.path.exists(output_file):
  769.                     os.remove(output_file)
  770.  
  771.                 # Syncing is done for this file, now check if the file should
  772.                 # be deleted from the source (except when it was a deletion
  773.                 # that has been synced, then a deletion of the source file
  774.                 # does not make sense, of course).
  775.                 for rule in self.rules:
  776.                     if event != FSMonitor.DELETED:
  777.                         if rule["deletionDelay"] is None:
  778.                             self.logger.debug("Not going to delete '%s'." % (input_file))
  779.                         elif rule["deletionDelay"] > 0:
  780.                             self.lock.acquire()
  781.                             self.files_to_delete.append((input_file, time.time() + rule["deletionDelay"]))
  782.                             self.logger.warning("Scheduled '%s' for deletion in %d seconds, as per the '%s' rule." % (input_file, rule["deletionDelay"], rule["label"]))
  783.                             self.lock.release()
  784.                         else:
  785.                             if os.path.exists(input_file):
  786.                                 os.remove(input_file)
  787.                             self.logger.warning("Deleted '%s' as per the '%s' rule." % (input_file, rule["label"]))
  788.  
  789.                 # The file went all the way through the pipeline, so now it's safe
  790.                 # to remove it from the persistent 'files_in_pipeline' list.
  791.                 self.lock.acquire()
  792.                 self.files_in_pipeline.remove((input_file, event))
  793.                 self.lock.release()
  794.                 self.logger.warning("Synced: '%s' (%s)." % (input_file, FSMonitor.EVENTNAMES[event]))
  795.  
  796.         processed += 1
  797.  
  798.  
  799.     def __process_files_to_delete(self):
  800.         processed = 0
  801.  
  802.         current_time = time.time()
  803.  
  804.         # Get a list of all files that can be deleted *now*
  805.         if len(self.files_to_delete) > 0:
  806.             files_to_delete_now = []
  807.             self.lock.acquire()
  808.             for (input_file, deletion_time) in self.files_to_delete:
  809.                 if deletion_time <= current_time:
  810.                     files_to_delete_now.append((input_file, deletion_time))
  811.                     # Stop when we reach the batch size.
  812.                     if len(files_to_delete_now) == QUEUE_PROCESS_BATCH_SIZE:
  813.                         break
  814.             self.lock.release()
  815.  
  816.             # Delete files and remove them from the persistent list.
  817.             for (input_file, deletion_time) in files_to_delete_now:
  818.                 if os.path.exists(input_file):
  819.                     os.remove(input_file)
  820.  
  821.                 self.lock.acquire()
  822.                 self.files_to_delete.remove((input_file, deletion_time))
  823.                 self.lock.release()
  824.  
  825.                 self.logger.warning("Deleted '%s', which was scheduled for deletion %d seconds ago." % (input_file, current_time - deletion_time))
  826.  
  827.                 processed += 1
  828.  
  829.  
  830.     def __process_retry_queue(self):
  831.         processed = 0
  832.  
  833.         while processed < QUEUE_PROCESS_BATCH_SIZE and self.retry_queue.qsize() > 0:
  834.             # Retry queue -> failed files list.
  835.             # And remove from files in pipeline.
  836.             self.lock.acquire()
  837.             (input_file, event) = self.retry_queue.get()
  838.             # It's possible that the file is already in the failed_files
  839.             # persistent list or in the pipeline queue (if it is being retried
  840.             # already) if it is being processed per server and now a second
  841.             # (or third or ...) processor is requesting a retry.
  842.             if (input_file, event) not in self.failed_files and (input_file, event) not in self.pipeline_queue:
  843.                 self.failed_files.append((input_file, event))
  844.                 already_in_failed_files = False
  845.             else:
  846.                 already_in_failed_files = True
  847.             self.files_in_pipeline.remove((input_file, event))
  848.             self.lock.release()
  849.  
  850.             # Log.
  851.             if not already_in_failed_files:
  852.                 self.logger.warning("Retry queue -> 'failed_files' persistent list: '%s'. Retrying later." % (input_file))
  853.             else:
  854.                 self.logger.warning("Retry queue -> 'failed_files' persistent list: '%s'. File already being retried later." % (input_file))
  855.             processed += 1
  856.  
  857.  
  858.     def __allow_retry(self):
  859.         num_failed_files = len(self.failed_files)
  860.         should_retry = self.last_retry + RETRY_INTERVAL < time.time()
  861.         pipeline_queue_almost_empty = self.pipeline_queue < MAX_FILES_IN_PIPELINE
  862.        
  863.         if num_failed_files > 0 and (should_retry or pipeline_queue_almost_empty):
  864.             failed_items = []
  865.  
  866.             processed = 0
  867.             while processed < QUEUE_PROCESS_BATCH_SIZE and processed < len(self.failed_files):
  868.                 item = self.failed_files[processed]
  869.                 failed_items.append(item)
  870.                 self.pipeline_queue.put(item)
  871.                 processed += 1
  872.            
  873.             for item in failed_items:
  874.                 self.failed_files.remove(item)
  875.  
  876.             self.last_retry = time.time()
  877.  
  878.             # Log.
  879.             self.logger.warning("Moved %d items from the 'failed_files' persistent list into the 'pipeline' persistent queue." % (processed))
  880.  
  881.  
  882.     def __get_transporter(self, server):
  883.         """get a transporter; if one is ready for new work, use that one,
  884.        otherwise try to start a new transporter"""
  885.  
  886.         # Try to find a running transporter that is ready for new work.
  887.         for id in range(0, len(self.transporters[server])):
  888.             transporter = self.transporters[server][id]
  889.             # Don't put more than MAX_TRANSPORTER_QUEUE_SIZE files in each
  890.             # transporter's queue.
  891.             if transporter.qsize() <= MAX_TRANSPORTER_QUEUE_SIZE:
  892.                 place_in_queue = transporter.qsize() + 1
  893.                 return (id, place_in_queue, transporter)
  894.  
  895.         # Don't run more than the allowed number of simultaneous transporters.
  896.         if not self.transporters_running < MAX_SIMULTANEOUS_TRANSPORTERS:
  897.             return (None, None, None)
  898.  
  899.         # Don't run more transporters for each server than its "maxConnections"
  900.         # setting allows.
  901.         num_connections = len(self.transporters[server])
  902.         max_connections = self.config.servers[server]["maxConnections"]
  903.         if max_connections == 0 or num_connections < max_connections:
  904.             transporter    = self.__create_transporter(server)
  905.             id             = len(self.transporters[server]) - 1
  906.             # If a transporter was succesfully created, add it to the pool.
  907.             if transporter:
  908.                 self.transporters[server].append(transporter)
  909.                 transporter.start()
  910.                 self.transporters_running += 1
  911.                 # Since this transporter was just created, it's obvious that we're
  912.                 # first in line.
  913.                 place_in_queue = 1
  914.                 return (id, 1, transporter)
  915.  
  916.         return (None, None, None)
  917.  
  918.  
  919.     def __create_transporter(self, server):
  920.         """create a transporter for the given server"""
  921.  
  922.         transporter_name = self.config.servers[server]["transporter"]
  923.         settings = self.config.servers[server]["settings"]
  924.         transporter_class = self._import_transporter(transporter_name)
  925.  
  926.         # Attempt to create an instance of the transporter.
  927.         try:
  928.             transporter = transporter_class(settings, self.transporter_callback, self.transporter_error_callback, "Arbitrator")
  929.         except ConnectionError, e:
  930.             self.logger.error("Could not start transporter '%s'. Error: '%s'." % (transporter_name, e))
  931.             return False
  932.         else:
  933.             self.logger.warning("Created '%s' transporter for the '%s' server." % (transporter_name, server))
  934.  
  935.         return transporter
  936.  
  937.  
  938.     def __calculate_transporter_dst(self, src, parent_path=None, relative_paths=[]):
  939.         dst = src
  940.  
  941.         # Strip off any relative paths.
  942.         for relative_path in relative_paths:
  943.             if dst.startswith(relative_path):
  944.                 dst = dst[len(relative_path):]
  945.  
  946.         # Ensure no absolute path is returned, which would make os.path.join()
  947.         # fail.
  948.         dst = dst.lstrip(os.sep)
  949.  
  950.         # Prepend any possible parent path.
  951.         if not parent_path is None:
  952.             dst = os.path.join(parent_path, dst)
  953.  
  954.         return dst
  955.  
  956.  
  957.     def fsmonitor_callback(self, monitored_path, event_path, event, discovered_through):
  958.         # Map FSMonitor's variable names to ours.
  959.         input_file = event_path
  960.  
  961.         if CALLBACKS_CONSOLE_OUTPUT:
  962.             print """FSMONITOR CALLBACK FIRED:
  963.                    input_file='%s'
  964.                    event=%d"
  965.                    discovered_through=%s""" % (input_file, event, discovered_through)
  966.  
  967.         # The file may have already been deleted!
  968.         deleted = event == FSMonitor.DELETED
  969.         touched = event == FSMonitor.CREATED or event == FSMonitor.MODIFIED
  970.         if deleted or (touched and os.path.exists(event_path)):
  971.             # Ignore directories (we cannot test deleted files to see if they
  972.             # are directories, because they obviously don't exist anymore).
  973.             if touched:
  974.                 try:
  975.                     if stat.S_ISDIR(os.stat(event_path)[stat.ST_MODE]):
  976.                         return
  977.                 except OSError, e:
  978.                     # The file (or directory, we can't be sure at this point)
  979.                     # does no longer exist (despite the os.path.exists() check
  980.                     # above!): it must *just* have been deleted.
  981.                     if e.errno == os.errno.ENOENT:
  982.                         return
  983.  
  984.             # Map FSMonitor's variable names to ours.
  985.             input_file = event_path
  986.  
  987.             # Add to discover queue.
  988.             self.lock.acquire()
  989.             self.discover_queue.put((input_file, event))
  990.             self.lock.release()
  991.  
  992.  
  993.     def processor_chain_callback(self, input_file, output_file, event, rule, processed_for_server):
  994.         if CALLBACKS_CONSOLE_OUTPUT:
  995.             print """PROCESSOR CHAIN CALLBACK FIRED:
  996.                    input_file='%s'
  997.                    (curried): event=%d
  998.                    (curried): rule='%s'
  999.                    (curried): processed_for_server='%s'
  1000.                    output_file='%s'""" % (input_file, event, rule["label"], processed_for_server, output_file)
  1001.  
  1002.         # Decrease number of running processor chains.
  1003.         self.lock.acquire()
  1004.         self.processorchains_running -= 1
  1005.         self.lock.release()
  1006.  
  1007.         # If the input file was not processed for a specific server, then it
  1008.         # should be synced to all destinations. Else, it should only be synced
  1009.         # to the server it was processed for.
  1010.         if processed_for_server == Arbitrator.PROCESSED_FOR_ANY_SERVER:
  1011.             for server in rule["destinations"].keys():
  1012.                 # Add to transport queue.
  1013.                 self.lock.acquire()
  1014.                 self.transport_queue[server].put((input_file, event, rule, processed_for_server, output_file))
  1015.                 self.lock.release()
  1016.             self.logger.info("Process queue -> transport queue: '%s'." % (input_file))
  1017.         else:
  1018.             # Add to transport queue.
  1019.             self.lock.acquire()
  1020.             self.transport_queue[processed_for_server].put((input_file, event, rule, processed_for_server, output_file))
  1021.             self.lock.release()
  1022.             self.logger.info("Process queue -> transport queue: '%s' (processed for server '%s')." % (input_file, processed_for_server))
  1023.  
  1024.  
  1025.     def processor_chain_error_callback(self, input_file, event):
  1026.         if CALLBACKS_CONSOLE_OUTPUT:
  1027.             print """PROCESSOR CHAIN ERROR CALLBACK FIRED:
  1028.                    input_file='%s'
  1029.                    (curried): event=%d""" % (input_file, event)
  1030.  
  1031.         # Add to retry queue.
  1032.         self.lock.acquire()
  1033.         self.retry_queue.put((input_file, event))
  1034.         self.processorchains_running -= 1
  1035.         self.lock.release()
  1036.  
  1037.  
  1038.     def transporter_callback(self, src, dst, url, action, input_file, event, rule, processed_for_server, server):
  1039.         # Map Transporter's variable names to ours.
  1040.         output_file      = src
  1041.         transported_file = dst
  1042.  
  1043.         if CALLBACKS_CONSOLE_OUTPUT:
  1044.             print """TRANSPORTER CALLBACK FIRED:
  1045.                    (curried): input_file='%s'
  1046.                    (curried): event=%d
  1047.                    (curried): rule='%s'
  1048.                    (curried): processed_for_server='%s'
  1049.                    output_file='%s'
  1050.                    transported_file='%s'
  1051.                    url='%s'
  1052.                    server='%s'""" % (input_file, event, rule["label"], processed_for_server, output_file, transported_file, url, server)
  1053.  
  1054.         # Add to db queue.
  1055.         self.lock.acquire()
  1056.         self.db_queue.put((input_file, event, rule, processed_for_server, output_file, transported_file, url, server))
  1057.         self.lock.release()
  1058.  
  1059.         self.logger.info("Transport queue -> DB queue: '%s' (server: '%s')." % (input_file, server))
  1060.  
  1061.  
  1062.     def transporter_error_callback(self, src, dst, action, input_file, event):
  1063.         if CALLBACKS_CONSOLE_OUTPUT:
  1064.             print """TRANSPORTER ERROR CALLBACK FIRED:
  1065.                    (curried): input_file='%s'
  1066.                    (curried): event=%d""" % (input_file, event)
  1067.  
  1068.         self.retry_queue.put((input_file, event))
  1069.  
  1070.  
  1071.     def stop(self):
  1072.         # Everybody dies only once.
  1073.         self.lock.acquire()
  1074.         if self.die:
  1075.             self.lock.release()
  1076.             return
  1077.         self.lock.release()
  1078.  
  1079.         # Die.
  1080.         self.logger.warning("Signaling to stop.")
  1081.         self.lock.acquire()
  1082.         self.die = True
  1083.         self.lock.release()
  1084.  
  1085.  
  1086.     def clean_up_working_dir(self):
  1087.         for root, dirs, files in os.walk(WORKING_DIR, topdown=False):
  1088.             for name in files:
  1089.                 os.remove(os.path.join(root, name))
  1090.             for name in dirs:
  1091.                 os.rmdir(os.path.join(root, name))
  1092.         self.logger.info("Cleaned up the working directory '%s'." % (WORKING_DIR))
  1093.  
  1094.  
  1095.     def _import_processor(self, processor):
  1096.         """Imports processor module and class, returns class.
  1097.  
  1098.        Input value can be:
  1099.  
  1100.        * a full/absolute class path, like
  1101.          "MyProcessorPackage.SomeProcessorClass"
  1102.        * a class path relative to fileconveyor.processors, like
  1103.          "image_optimizer.KeepFilename"
  1104.        """
  1105.         processor_class = None
  1106.         module = None
  1107.         alternatives = [processor]
  1108.         default_prefix = 'processors.' # Not 'fileconveyor.processors.'!
  1109.         if not processor.startswith(default_prefix):
  1110.             alternatives.append('%s%s' % (default_prefix, processor))
  1111.         for processor_name in alternatives:
  1112.             (modulename, classname) = processor_name.rsplit(".", 1)
  1113.             try:
  1114.                 module = __import__(modulename, globals(), locals(), [classname])
  1115.             except ImportError:
  1116.                 pass
  1117.         if not module:
  1118.             msg = "The processor module '%s' could not be found." % processor
  1119.             if len(alternatives) > 1:
  1120.                 msg = '%s Tried (%s)' % (msg, ', '.join(alternatives))
  1121.             self.logger.error(msg)
  1122.         else:
  1123.             try:
  1124.                 processor_class = getattr(module, classname)
  1125.             except AttributeError:
  1126.                 self.logger.error("The Processor module '%s' was found, but its Processor class '%s' could not be found."  % (modulename, classname))
  1127.         return processor_class
  1128.  
  1129.  
  1130.     def _import_transporter(self, transporter):
  1131.         """Imports transporter module and class, returns class.
  1132.  
  1133.        Input value can be:
  1134.  
  1135.        * a full/absolute module path, like
  1136.          "MyTransporterPackage.SomeTransporterClass"
  1137.        * a module path relative to fileconveyor.transporters, like
  1138.          "symlink_or_copy"
  1139.        """
  1140.         transporter_class = None
  1141.         module = None
  1142.         alternatives = [transporter]
  1143.         default_prefix = 'transporters.transporter_' # Not 'fileconveyor.transporters.transporter_'!
  1144.         if not transporter.startswith(default_prefix):
  1145.             alternatives.append('%s%s' % (default_prefix, transporter))
  1146.         for module_name in alternatives:
  1147.             try:
  1148.                 module = __import__(module_name, globals(), locals(), ["TRANSPORTER_CLASS"], -1)
  1149.             except ImportError:
  1150.                 pass
  1151.         if not module:
  1152.             msg = "The transporter module '%s' could not be found." % transporter
  1153.             if len(alternatives) > 1:
  1154.                 msg = '%s Tried (%s)' % (msg, ', '.join(alternatives))
  1155.             self.logger.error(msg)
  1156.         else:
  1157.             try:
  1158.                 classname = module.TRANSPORTER_CLASS
  1159.                 module = __import__(module_name, globals(), locals(), [classname])
  1160.                 transporter_class = getattr(module, classname)
  1161.             except AttributeError:
  1162.                 self.logger.error("The Transporter module '%s' was found, but its Transporter class '%s' could not be found."  % (module_name))
  1163.         return transporter_class
  1164.  
  1165.  
  1166. def run_file_conveyor(restart=False):
  1167.     try:
  1168.         arbitrator = Arbitrator(os.path.join(FILE_CONVEYOR_PATH, "config.xml"), restart)
  1169.     except ArbitratorInitError, e:
  1170.         print e.__class__.__name__, e
  1171.     except ArbitratorError, e:
  1172.         print e.__class__.__name__, e
  1173.         del arbitrator
  1174.     else:
  1175.         t = DaemonThreadRunner(arbitrator, PID_FILE)
  1176.         t.start()
  1177.         del t
  1178.         del arbitrator
  1179.  
  1180.  
  1181. if __name__ == '__main__':
  1182.     if not RESTART_AFTER_UNHANDLED_EXCEPTION:
  1183.         run_file_conveyor()
  1184.     else:
  1185.         run_file_conveyor()
  1186.         # Don't restart File Conveyor, but actually quit it when it's stopped
  1187.         # by the user in the console. See DaemonThreadRunner.handle_signal()
  1188.         # for details.
  1189.         while True and not DaemonThreadRunner.stopped_in_console:
  1190.             # Make sure there's always a PID file, even when File Conveyor
  1191.             # technically isn't running.
  1192.             DaemonThreadRunner.write_pid_file(os.path.expanduser(PID_FILE))
  1193.             time.sleep(RESTART_INTERVAL)
  1194.             run_file_conveyor(restart=True)
Advertisement
Add Comment
Please, Sign In to add comment