[damned-lies] Implement module locking at update_stats level



commit 2f827de951d6dcca4e6ba74f4f282a09d7f9ee23
Author: Claude Paroz <claude 2xlibre net>
Date:   Mon Sep 13 23:03:25 2010 +0200

    Implement module locking at update_stats level
    
    Also use the new context_manager functionality of python 2.5 and the
    accompanying with statement

 stats/management/commands/update-stats.py |   28 +---
 stats/models.py                           |  287 ++++++++++++++++-------------
 2 files changed, 156 insertions(+), 159 deletions(-)
---
diff --git a/stats/management/commands/update-stats.py b/stats/management/commands/update-stats.py
index cf47f84..2bafe4c 100644
--- a/stats/management/commands/update-stats.py
+++ b/stats/management/commands/update-stats.py
@@ -1,5 +1,4 @@
-import sys, os, traceback
-import time
+import sys, traceback
 from optparse import make_option
 from django.core.management.base import BaseCommand
 from django.core.mail import mail_admins
@@ -33,14 +32,11 @@ class Command(BaseCommand):
                     return "Update unsuccessful."
                 print "Updating stats for %s.%s..." % (module_arg, branch_arg)
                 try:
-                    self.get_lock_for_module(module_arg, branch_arg)
                     branch.update_stats(options['force'])
                 except:
                     tbtext = traceback.format_exc()
                     mail_admins("Error while updating %s %s" % (module_arg, branch_arg), tbtext)
                     print >> sys.stderr, "Error during updating, mail sent to admins"
-                finally:
-                    self.release_lock_for_module(module_arg, branch_arg)
 
         elif len(args) == 1:
             # Update all branches of a module
@@ -49,13 +45,10 @@ class Command(BaseCommand):
             branches = Branch.objects.filter(module__name=module_arg)
             for branch in branches.all():
                 try:
-                    self.get_lock_for_module(module_arg, branch.name)
                     branch.update_stats(options['force'])
                 except:
                     print >> sys.stderr, traceback.format_exc()
                     print "Error while updating stats for %s (branch '%s')" % (module_arg, branch.name)
-                finally:
-                    self.release_lock_for_module(module_arg, branch.name)
         else:
             # Update all modules
             if options['non-gnome']:
@@ -67,28 +60,9 @@ class Command(BaseCommand):
                 branches = Branch.objects.filter(module__name=mod)
                 for branch in branches.all():
                     try:
-                        self.get_lock_for_module(mod.name, branch.name)
                         branch.update_stats(options['force'])
                     except:
                         print >> sys.stderr, traceback.format_exc()
                         print "Error while updating stats for %s (branch '%s')" % (mod.name, branch.name)
-                    finally:
-                        self.release_lock_for_module(mod.name, branch.name)
 
         return "Update completed."
-
-    # Weird things happen when multiple updates run in parallel for the same module
-    # We use filesystem directories creation/deletion to act as global lock mecanism
-    def get_lock_for_module(self, module_name, branch_name):
-        dirpath = os.path.join("/tmp", "updating-%s" % (module_name,))
-        while True:
-            try:
-                os.mkdir(dirpath)
-                break;
-            except OSError:
-                time.sleep(60)
-        return # Lock acquired
-
-    def release_lock_for_module(self, module_name, branch_name):
-        dirpath = os.path.join("/tmp", "updating-%s" % (module_name,))
-        os.rmdir(dirpath)
diff --git a/stats/models.py b/stats/models.py
index 7a55a99..0561237 100644
--- a/stats/models.py
+++ b/stats/models.py
@@ -19,8 +19,10 @@
 # along with Damned Lies; if not, write to the Free Software Foundation, Inc.,
 # 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
+from __future__ import with_statement
 import os, sys, re, hashlib
 import threading
+from time import sleep
 from datetime import datetime
 
 from django.conf import settings
@@ -128,6 +130,26 @@ class Module(models.Model):
             return True
         return False
 
+class ModuleLock(object):
+    """ Weird things happen when multiple updates run in parallel for the same module
+        We use filesystem directories creation/deletion to act as global lock mecanism
+    """
+    def __init__(self, mod):
+        assert isinstance(mod, Module)
+        self.module = mod
+
+    def __enter__(self):
+        self.dirpath = os.path.join("/tmp", "updating-%s" % (self.module.name,))
+        while True:
+            try:
+                os.mkdir(self.dirpath)
+                break;
+            except OSError:
+                sleep(30)
+
+    def __exit__(self, *exc_info):
+        os.rmdir(self.dirpath)
+
 class Branch(models.Model):
     """ Branch of a module """
     name        = models.CharField(max_length=50)
@@ -296,145 +318,146 @@ class Branch(models.Model):
 
     def update_stats(self, force, checkout=True):
         """ Update statistics for all po files from the branch """
-        if checkout:
-            self.checkout()
-        domains = Domain.objects.filter(module=self.module).all()
-        string_frozen = self.has_string_frozen()
-        for dom in domains:
-            # 1. Initial settings
-            # *******************
-            domain_path = os.path.join(self.co_path(), dom.directory)
-            if not os.access(domain_path, os.X_OK):
-                # TODO: should check if existing stats, and delete (archive) them in this case
-                continue
-            errors = []
-
-            # 2. Pre-check, if available (intltool-update -m)
-            # **************************
-            if dom.dtype == 'ui' and not dom.pot_method:
-                # Run intltool-update -m to check for some errors
-                errors.extend(utils.check_potfiles(domain_path))
-
-            # 3. Generate a fresh pot file
-            # ****************************
-            if dom.dtype == 'ui':
-                potfile, errs = dom.generate_pot_file(self)
-            elif dom.dtype == 'doc':
-                if dom.pot_method:
-                    potfile, errs = dom.generate_pot_file(self)
-                else:
-                    # Standard gnome-doc-utils pot generation
-                    potfile, errs = utils.generate_doc_pot_file(domain_path, dom.potbase(), self.module.name, settings.DEBUG)
-                if not potfile:
-                    print >> sys.stderr, "\n".join([e[1] for e in errs])
-                    continue
-            else:
-                print >> sys.stderr, "Unknown domain type '%s', ignoring domain '%s'" % (dom.dtype, dom.name)
-                continue
-            errors.extend(errs)
-            linguas = dom.get_linguas(self.co_path())
-            if linguas['langs'] is None and linguas['error']:
-                errors.append(("warn", linguas['error']))
-
-            # Prepare statistics object
-            try:
-                stat = Statistics.objects.get(language=None, branch=self, domain=dom)
-                Information.objects.filter(statistics=stat).delete() # Reset errors
-            except Statistics.DoesNotExist:
-                stat = Statistics(language=None, branch=self, domain=dom)
-                stat.save()
-
-            # 4. Compare with old pot files, various checks
-            # *****************************
-            previous_pot = os.path.join(self.output_dir(dom.dtype), dom.potbase() + "." + self.name + ".pot")
-            if not potfile:
-                if settings.DEBUG: print >> sys.stderr, "Can't generate POT file for %s/%s." % (self.module.name, dom.directory)
-                if os.access(previous_pot, os.R_OK):
-                    # Use old POT file
-                    potfile = previous_pot
-                    errors.append(("error", ugettext_noop("Can't generate POT file, using old one.")))
-                else:
-                    errors.append(("error", ugettext_noop("Can't generate POT file, statistics aborted.")))
-                    stat.set_errors(errors)
+        with ModuleLock(self.module):
+            if checkout:
+                self.checkout()
+            domains = Domain.objects.filter(module=self.module).all()
+            string_frozen = self.has_string_frozen()
+            for dom in domains:
+                # 1. Initial settings
+                # *******************
+                domain_path = os.path.join(self.co_path(), dom.directory)
+                if not os.access(domain_path, os.X_OK):
+                    # TODO: should check if existing stats, and delete (archive) them in this case
                     continue
+                errors = []
+
+                # 2. Pre-check, if available (intltool-update -m)
+                # **************************
+                if dom.dtype == 'ui' and not dom.pot_method:
+                    # Run intltool-update -m to check for some errors
+                    errors.extend(utils.check_potfiles(domain_path))
 
-            changed_status = utils.CHANGED_WITH_ADDITIONS
-
-            if os.access(previous_pot, os.R_OK):
-                # Compare old and new POT
-                changed_status, diff = utils.pot_diff_status(previous_pot, potfile)
-                if string_frozen and dom.dtype == 'ui' and changed_status == utils.CHANGED_WITH_ADDITIONS:
-                    utils.notify_list("%s.%s" % (self.module.name, self.name), diff)
-
-                if changed_status != utils.NOT_CHANGED:
-                    signals.pot_has_changed.send(sender=self, potfile=potfile, branch=self, domain=dom)
-
-            # 5. Generate pot stats and update DB
-            # ***********************************
-            pot_stats = utils.po_file_stats(potfile, False)
-            errors.extend(pot_stats['errors'])
-            if potfile != previous_pot and not utils.copy_file(potfile, previous_pot):
-                errors.append(('error', ugettext_noop("Can't copy new POT file to public location.")))
-
-            stat.set_translation_stats(previous_pot, untranslated=int(pot_stats['untranslated']), num_figures=int(pot_stats['num_figures']))
-            stat.set_errors(errors)
-
-            # 6. Update language po files and update DB
-            # *****************************************
-            command = "msgmerge --previous -o %(outpo)s %(pofile)s %(potfile)s"
-            stats_with_ext_errors = Statistics.objects.filter(branch=self, domain=dom, information__type__endswith='-ext')
-            langs_with_ext_errors = [stat.language.locale for stat in stats_with_ext_errors]
-            for lang, pofile in dom.get_lang_files(self.co_path()):
-                outpo = os.path.join(self.output_dir(dom.dtype), dom.potbase() + "." + self.name + "." + lang + ".po")
-
-                if not force and changed_status in (utils.NOT_CHANGED, utils.CHANGED_ONLY_FORMATTING) and os.access(outpo, os.R_OK) \
-                   and os.stat(pofile)[8] < os.stat(outpo)[8] and not lang in langs_with_ext_errors :
+                # 3. Generate a fresh pot file
+                # ****************************
+                if dom.dtype == 'ui':
+                    potfile, errs = dom.generate_pot_file(self)
+                elif dom.dtype == 'doc':
+                    if dom.pot_method:
+                        potfile, errs = dom.generate_pot_file(self)
+                    else:
+                        # Standard gnome-doc-utils pot generation
+                        potfile, errs = utils.generate_doc_pot_file(domain_path, dom.potbase(), self.module.name, settings.DEBUG)
+                    if not potfile:
+                        print >> sys.stderr, "\n".join([e[1] for e in errs])
+                        continue
+                else:
+                    print >> sys.stderr, "Unknown domain type '%s', ignoring domain '%s'" % (dom.dtype, dom.name)
                     continue
+                errors.extend(errs)
+                linguas = dom.get_linguas(self.co_path())
+                if linguas['langs'] is None and linguas['error']:
+                    errors.append(("warn", linguas['error']))
 
-                realcmd = command % {
-                    'outpo' : outpo,
-                    'pofile' : pofile,
-                    'potfile' : potfile,
-                    }
-                utils.run_shell_command(realcmd)
-
-                langstats = utils.po_file_stats(outpo, True)
-                if linguas['langs'] is not None and lang not in linguas['langs']:
-                    langstats['errors'].append(("warn-ext", linguas['error']))
-                if dom.dtype == "doc":
-                    fig_stats = utils.get_fig_stats(outpo)
-                    for fig in fig_stats:
-                        trans_path = os.path.join(domain_path, lang, fig['path'])
-                        if os.access(trans_path, os.R_OK):
-                            fig_file = open(trans_path, 'rb').read()
-                            trans_hash = hashlib.md5(fig_file).hexdigest()
-                            if fig['hash'] == trans_hash:
-                                langstats['errors'].append(("warn-ext", "Figures should not be copied when identical to original (%s)." % trans_path))
-
-                if settings.DEBUG: print >>sys.stderr, lang + ":\n" + str(langstats)
-                # Save in DB
+                # Prepare statistics object
                 try:
-                    stat = Statistics.objects.get(language__locale=lang, branch=self, domain=dom)
-                    Information.objects.filter(statistics=stat).delete()
+                    stat = Statistics.objects.get(language=None, branch=self, domain=dom)
+                    Information.objects.filter(statistics=stat).delete() # Reset errors
                 except Statistics.DoesNotExist:
-                    try:
-                        language = Language.objects.get(locale=lang)
-                    except Language.DoesNotExist:
-                        if self.is_head():
-                            language = Language(name=lang, locale=lang)
-                            language.save()
-                        else:
-                            # Do not create language (and therefore ignore stats) for an 'old' branch
-                            continue
-                    stat = Statistics(language = language, branch = self, domain = dom)
+                    stat = Statistics(language=None, branch=self, domain=dom)
                     stat.save()
-                stat.set_translation_stats(outpo,
-                                           translated = int(langstats['translated']),
-                                           fuzzy = int(langstats['fuzzy']),
-                                           untranslated = int(langstats['untranslated']),
-                                           num_figures = int(langstats['num_figures']))
-                for err in langstats['errors']:
-                    stat.information_set.add(Information(type=err[0], description=err[1]))
+
+                # 4. Compare with old pot files, various checks
+                # *****************************
+                previous_pot = os.path.join(self.output_dir(dom.dtype), dom.potbase() + "." + self.name + ".pot")
+                if not potfile:
+                    if settings.DEBUG: print >> sys.stderr, "Can't generate POT file for %s/%s." % (self.module.name, dom.directory)
+                    if os.access(previous_pot, os.R_OK):
+                        # Use old POT file
+                        potfile = previous_pot
+                        errors.append(("error", ugettext_noop("Can't generate POT file, using old one.")))
+                    else:
+                        errors.append(("error", ugettext_noop("Can't generate POT file, statistics aborted.")))
+                        stat.set_errors(errors)
+                        continue
+
+                changed_status = utils.CHANGED_WITH_ADDITIONS
+
+                if os.access(previous_pot, os.R_OK):
+                    # Compare old and new POT
+                    changed_status, diff = utils.pot_diff_status(previous_pot, potfile)
+                    if string_frozen and dom.dtype == 'ui' and changed_status == utils.CHANGED_WITH_ADDITIONS:
+                        utils.notify_list("%s.%s" % (self.module.name, self.name), diff)
+
+                    if changed_status != utils.NOT_CHANGED:
+                        signals.pot_has_changed.send(sender=self, potfile=potfile, branch=self, domain=dom)
+
+                # 5. Generate pot stats and update DB
+                # ***********************************
+                pot_stats = utils.po_file_stats(potfile, False)
+                errors.extend(pot_stats['errors'])
+                if potfile != previous_pot and not utils.copy_file(potfile, previous_pot):
+                    errors.append(('error', ugettext_noop("Can't copy new POT file to public location.")))
+
+                stat.set_translation_stats(previous_pot, untranslated=int(pot_stats['untranslated']), num_figures=int(pot_stats['num_figures']))
+                stat.set_errors(errors)
+
+                # 6. Update language po files and update DB
+                # *****************************************
+                command = "msgmerge --previous -o %(outpo)s %(pofile)s %(potfile)s"
+                stats_with_ext_errors = Statistics.objects.filter(branch=self, domain=dom, information__type__endswith='-ext')
+                langs_with_ext_errors = [stat.language.locale for stat in stats_with_ext_errors]
+                for lang, pofile in dom.get_lang_files(self.co_path()):
+                    outpo = os.path.join(self.output_dir(dom.dtype), dom.potbase() + "." + self.name + "." + lang + ".po")
+
+                    if not force and changed_status in (utils.NOT_CHANGED, utils.CHANGED_ONLY_FORMATTING) and os.access(outpo, os.R_OK) \
+                       and os.stat(pofile)[8] < os.stat(outpo)[8] and not lang in langs_with_ext_errors :
+                        continue
+
+                    realcmd = command % {
+                        'outpo' : outpo,
+                        'pofile' : pofile,
+                        'potfile' : potfile,
+                        }
+                    utils.run_shell_command(realcmd)
+
+                    langstats = utils.po_file_stats(outpo, True)
+                    if linguas['langs'] is not None and lang not in linguas['langs']:
+                        langstats['errors'].append(("warn-ext", linguas['error']))
+                    if dom.dtype == "doc":
+                        fig_stats = utils.get_fig_stats(outpo)
+                        for fig in fig_stats:
+                            trans_path = os.path.join(domain_path, lang, fig['path'])
+                            if os.access(trans_path, os.R_OK):
+                                fig_file = open(trans_path, 'rb').read()
+                                trans_hash = hashlib.md5(fig_file).hexdigest()
+                                if fig['hash'] == trans_hash:
+                                    langstats['errors'].append(("warn-ext", "Figures should not be copied when identical to original (%s)." % trans_path))
+
+                    if settings.DEBUG: print >>sys.stderr, lang + ":\n" + str(langstats)
+                    # Save in DB
+                    try:
+                        stat = Statistics.objects.get(language__locale=lang, branch=self, domain=dom)
+                        Information.objects.filter(statistics=stat).delete()
+                    except Statistics.DoesNotExist:
+                        try:
+                            language = Language.objects.get(locale=lang)
+                        except Language.DoesNotExist:
+                            if self.is_head():
+                                language = Language(name=lang, locale=lang)
+                                language.save()
+                            else:
+                                # Do not create language (and therefore ignore stats) for an 'old' branch
+                                continue
+                        stat = Statistics(language = language, branch = self, domain = dom)
+                        stat.save()
+                    stat.set_translation_stats(outpo,
+                                               translated = int(langstats['translated']),
+                                               fuzzy = int(langstats['fuzzy']),
+                                               untranslated = int(langstats['untranslated']),
+                                               num_figures = int(langstats['num_figures']))
+                    for err in langstats['errors']:
+                        stat.information_set.add(Information(type=err[0], description=err[1]))
 
     def _exists(self):
         """ Determine if branch (self) already exists (i.e. already checked out) on local FS """



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]