2010-11-18 5 views
0

Je travaille sur un projet sur un système non fiable qui, je suppose, peut échouer à tout moment. Ce que je veux garantir, c'est que si write_state et la machine échouent au milieu de l'opération, read_state lira un état valide ou aucun état du tout. J'ai mis en place quelque chose qui, je pense, travaillera ci-dessous - je m'intéresse à la critique de cela ou à des solutions alternatives si quelqu'un en connaît une.Stockage d'état atomique en Python?

Mon idée:

import hashlib, cPickle, os 

def write_state(logname, state): 
    state_string = cPickle.dumps(state, cPickle.HIGHEST_PROTOCOL) 
    state_string += hashlib.sha224(state_string).hexdigest() 

    handle = open('%s.1' % logname, 'wb') 
    handle.write(state_string) 
    handle.close() 

    handle = open('%s.2' % logname, 'wb') 
    handle.write(state_string) 
    handle.close() 

def get_state(logname): 
    def read_file(name): 
     try: 
      f = open(name,'rb') 
      data = f.read() 
      f.close() 
      return data 
     except IOError: 
      return '' 
    def parse(data): 
     if len(data) < 56: 
      return (None, '', False) 
     hash = data[-56:] 
     data = data[:-56] 
     valid = hashlib.sha224(data).hexdigest() == hash 
     try: 
      parsed = cPickle.loads(data) 
     except cPickle.UnpicklingError: 
      parsed = None 
     return (parsed, valid) 

    data1,valid1 = parse(read_file('%s.1'%logname)) 
    data2,valid2 = parse(read_file('%s.2'%logname)) 

    if valid1 and valid2: 
     return data1 
    elif valid1 and not valid2: 
     return data1 
    elif valid2 and not valid1: 
     return data2 
    elif not valid1 and not valid2: 
     raise Exception('Theoretically, this never happens...') 

.: par exemple

write_state('test_log', {'x': 5}) 
print get_state('test_log') 

Répondre

3

Vos deux copies ne fonctionneront pas. Le système de fichiers peut réorganiser les choses de sorte que les deux fichiers ont été tronqués avant d'avoir été écrits sur le disque.

Il y a quelques opérations de système de fichiers dont la garantie est atomique: renommer un fichier par-dessus un autre en est un, dans la mesure où le fichier sera à un endroit ou à un autre.Cependant, en ce qui concerne POSIX, cela ne garantit pas que le déplacement est effectué avant que le contenu du fichier ne soit sur le disque, ce qui signifie qu'il ne vous donne que le verrouillage.

Les systèmes de fichiers Linux ont imposé que le contenu du fichier frappe le disque avant que le déplacement atomique ne le fasse (mais pas de manière synchrone), ce qui fait ce que vous voulez. ext4 a cassé cette hypothèse pendant un court moment, rendant ces fichiers plus susceptibles de se retrouver vides. C'était widely regarded as a dick move, et a été remédié depuis. De toute façon, la façon correcte de faire ceci est: créer un fichier temporaire dans le même répertoire (donc c'est sur le même système de fichiers); écrire de nouvelles données; fsync le fichier temporaire; Renommez-le par rapport à la version précédente. C'est aussi atomique que l'OS peut le garantir. Cela vous donne également de la durabilité au prix de la rotation des disques, ce qui explique pourquoi les développeurs d'applications préfèrent ne pas utiliser fsync et mettre à l'index les versions ext4 offensantes.

+0

Le fichier file.close() ne doit pas appeler un fsync? Ou n'est-ce pas synchrone? – sbirch

+0

Non. fsync signifie bloquer jusqu'à ce que cela frappe le disque. C'est cher car il tourne le disque. Il peut aussi être lent si vous avez beaucoup de données entrantes, et tout doit être écrit avant que votre fichier ne le puisse (quelle ext3 avec données = garanties commandées). fermer est bon marché et asynchrone. – Tobu

+0

Pourquoi ne pas simplement fsync les fichiers eux-mêmes? – sbirch

0

Je pense que vous pouvez simplifier quelques choses

def read_file(name): 
    try: 
     with open(name,'rb') as f 
      return f.read() 
    except IOError: 
     return '' 

if valid1: 
    return data1 
elif valid2: 
    return data2 
else: 
    raise Exception('Theoretically, this never happens...') 

Vous n'avez probablement pas besoin d'écrire les deux fichiers tout le temps, écrivez simplement file2 et renommez-le en file1.

Je pense qu'il ya encore une chance qu'un redémarrage à froid (par exemple coupure de courant) pourrait provoquer à la fois les fichiers de ne pas être écrites sur le disque correctement en raison de l'écriture différée

+0

Les deux fichiers qui ne sont pas écrits sont bien - je veux que ce soit atomique, donc l'échec complet est une option. Je pense que deux copies complètes sont nécessaires pour cette raison exactement - je ne veux pas une écriture ratée pour corrompre un déjà existant. – sbirch

+0

En y réfléchissant, je veux aussi que le système soit durable (comme dans le D d'ACID) - c'est pourquoi j'ai besoin de deux copies. – sbirch

+0

'data = ''; avec open (...) comme f: data = f.read(); return data' est encore meilleur (mais avec newlines au lieu de ';' s). – detly

1

Mon vague souvenir du travail de bases de données à sens unique est la suivante. Cela implique trois fichiers. Un fichier de contrôle, le fichier de base de données cible et un journal de transactions en attente.

Le fichier de contrôle possède un compteur de transactions global et un total de contrôle de hachage ou autre. C'est un petit fichier de taille physique. Une écriture au niveau du système d'exploitation. Avoir un compteur de transactions global dans votre fichier cible avec les données réelles, plus un hachage ou une autre somme de contrôle.

Avoir un journal de transactions en attente qui se développe juste ou est une file d'attente circulaire d'une taille finie, ou peut-être survole. Cela n'a pas beaucoup d'importance.

  1. Consigne toutes les transactions en attente dans le journal simple. Il y a un numéro de séquence et le contenu du changement.

  2. Mettez à jour le compteur de transactions, mettez à jour le hachage dans le fichier de contrôle. On écrit, rougit. Si cela échoue, alors rien n'a changé. Si cela réussit, le fichier de contrôle et le fichier cible ne correspondent pas, indiquant qu'une transaction a été démarrée mais pas terminée.

  3. Effectuez la mise à jour attendue sur le fichier cible. Cherchez au début et mettez à jour le compteur et la somme de contrôle. Si cela échoue, le fichier de contrôle a un compteur un de plus que le fichier cible. Le fichier cible est endommagé. Lorsque cela fonctionne, la dernière transaction enregistrée, le fichier de contrôle et le fichier cible sont tous d'accord sur le numéro de séquence.

Vous pouvez récupérer en relisant le journal, puisque vous connaissez le dernier bon numéro de séquence.

1

Sous les systèmes UNIX, la réponse habituelle est de faire le link dance. Créez le fichier sous un nom unique (utilisez le module tmpfile) puis utilisez la fonction os.link() pour créer un lien physique vers le nom de destination après avoir synchronisé le contenu dans l'état souhaité (publication). Dans ce schéma, vos lecteurs ne voient pas le fichier tant que l'état n'est pas sain. L'opération de lien est atomique. Vous pouvez dissocier le nom temporaire une fois que vous avez réussi à le lier au nom "prêt". Il y a quelques rides supplémentaires à gérer si vous avez besoin de garantir la sémantique sur les anciennes versions de NFS sans dépendre des démons de verrouillage.

+0

Il me semble me rappeler que le pli supplémentaire sur NFS implique l'ouverture du lien cible et l'exécution d'un fstat() sur le descripteur de fichier ouvert pour comparer son tuple d'inode dev à celui du fichier temp d'origine. Un décalage signifie que votre processus a perdu la course à un autre. –

1

Je vais ajouter une réponse hérétique: qu'en est-il de l'utilisation de sqlite? Ou, éventuellement, bsddb, cependant cela semble être obsolète et vous devrez utiliser un module tiers.

+0

Ouais j'ai pensé à ça, mais ça semblait un peu lourd pour ça - en plus les réponses sont intéressantes. – sbirch