2009-11-06 5 views

Répondre

3

Votre algorithme est OK (thread sûr dans la mesure où votre module DB API est sûr) et est probablement la meilleure voie à suivre. Il ne vous donnera jamais de doublon (en supposant que vous ayez une clé PRIMARY ou UNIQUE sur Sid), mais vous avez une chance négligeable d'obtenir l'exception IntegrityError sur INSERT. Mais votre code ne semble pas bon. Il est préférable d'utiliser une boucle avec un nombre limité de tentatives au lieu de récursion (qui, en cas d'une erreur dans le code pourrait devenir infini):

for i in range(MAX_ATTEMPTS): 
    sid = os.urandom(8).decode('hex') 
    db.execute('SELECT COUNT(*) FROM sessions WHERE sid=?', (sid,)) 
    if not db.fetchone()[0]: 
     # You can catch IntegrityError here and continue, but there are reasons 
     # to avoid this. 
     db.execute('INSERT INTO sessions (sid) VALUES (?)', (sid,)) 
     break 
else: 
    raise RuntimeError('Failed to generate unique session ID') 

Vous pouvez augmenter le nombre de caractères lecture aléatoire utilisé pour faire la chance de échouer encore plus petit. base64.urlsafe_b64encode() est votre ami si vous souhaitez raccourcir SID, mais assurez-vous que votre base de données utilise une comparaison sensible à la casse pour cette colonne (VARCHAR de MySQL ne convient pas sauf si vous définissez un classement binaire, mais VARBINARY est OK).

+0

Pouvez-vous expliquer, s'il vous plaît, quelles sont les raisons pour éviter d'attraper IntegrityError (premier commentaire dans votre code)? La condition semble être un peu moins fiable, car il y a toujours une petite chance d'obtenir IntegrityError lors de l'exécution de la requête. Est-ce que ces raisons de performance? – Tony

+0

@Anton certaines bases de données n'autorisent pas les instructions SQL suivantes après 'IntegrityError' jusqu'à la fin de la transaction. Vous devez donc revenir en arrière et répéter toutes les étapes depuis le début de la transaction. Ce n'est pas difficile dans certains cas particuliers (par exemple lorsqu'il n'y a pas d'autres requêtes dans la transaction), mais il n'y a pas de solution générale. La condition de course dans ma solution a une chance négligeable de se produire même une fois pour la vie de la plupart des projets. –

1

Pas besoin d'appeler la base de données que je pense:

>>> import uuid 

# make a UUID based on the host ID and current time 
>>> uuid.uuid1() 
UUID('a8098c1a-f86e-11da-bd1a-00112444be1e') 

De this page.

+0

Est-il assez aléatoire pour que les gens ne peuvent pas prédire –

+0

de UUID doit être suffisamment aléatoire pour 99% des demandes, mais je ne pense pas que c'est cryptographiquement secure – Andomar

+0

UUID est conçu pour être globalement unique (ce qui n'est pas obligatoire ici), mais pas imprévisible (alors que c'est obligatoire pour les sessions). –

0

Si vous devez absolument vérifier uid contre la base de données et éviter les conditions de course, les transactions d'utilisation:

BEGIN TRANSACTION 
SELECT COUNT(*) FROM sessions WHERE sid=%s 
INSERT INTO sessions (sid,...) VALUES (%s,...) 
COMMIT 
+0

SELECT ne créera pas de verrous, donc cela fonctionnera de la même façon que si select est avant le début de trans. –

+0

Non. Imaginez ce qui se passe lorsque les deux threads ont simultanément exécuté SELECT pour le même Sid et reçu False. Vous aurez deux inserts l'un après l'autre. Envelopper une seule instruction dans une transaction n'a aucun sens. – yk4ever

1

Je commencerais avec un ID de fil unique et (en quelque sorte) concaténer que, avec un compteur de thread local , puis l'alimenter via un algorithme de hachage cryptographique.

+0

+1 pour la technique de hachage, bien qu'il existe des modules pour générer le GUID. –

+0

Qu'en est-il des processus multiples? (WSGIDaemonProcess processes = 2 threads = 5) Impossible de créer un identifiant de thread unique? Je pense que le verrouillage de thread est la seule solution? –

+0

Vous utilisez un autre morceau d'état (ID de processus unique) et concaténez-le avec l'ID unique de thread et un compteur local de thread, puis un hachage. Ne fonctionne que tant que vous êtes dans un territoire "pas très grand nombre de threads et de processus", car vous ne voulez probablement pas faire éclater 64 bits pour votre compteur, mais cela vous donnerait, disons, un compteur local 32 bits, puis 16 bits pour l'ID de processus et un autre 16 pour l'ID de thread, si bien dans l'espace de raisonnable. – Vatine

2

Si vous avez besoin de thread pourquoi ne pas vous mettre le générateur de nombres aléatoires une fonction qui utilise un verrou partagé:

import threading 
lock = threading.Lock() 
def get_random_number(lock) 
    with lock: 
     print "This can only be done by one thread at a time" 

Si tous les fils appelant get_random_number utiliser la même instance de verrouillage, alors que l'un d'entre eux au moment peut créer un nombre aléatoire.

Bien sûr, vous avez également créé un goulot d'étranglement dans votre application avec cette solution. Il existe d'autres solutions en fonction de vos besoins telles que la création de blocs d'identifiants uniques puis leur consommation en parallèle.

+0

Est-ce que 2 séparer WSGIDaemonProcesses chacun ayant son propre verrou, toujours être en mesure d'exécuter la même sélection? –

5
import os, threading, Queue 

def idmaker(aqueue): 
    while True: 
    u = hexlify(os.urandom(8)).decode('ascii') 
    aqueue.put(u) 

idqueue = Queue.Queue(2) 

t = threading.Thread(target=idmaker, args=(idqueue,)) 
t.daemon = True 
t.start() 

def idgetter(): 
    return idqueue.get() 

file d'attente est souvent la meilleure façon de synchroniser les threads en Python - qui est assez fréquent que lors de la conception d'un système multi-thread votre première pensée devrait être « la meilleure façon que je pourrais le faire avec Queues ». L'idée sous-jacente est de dédier un thread pour entièrement "posséder" une ressource ou un sous-système partagé, et tous les autres threads "worker" accèdent à la ressource uniquement par get et/ou puts sur Files d'attente utilisées par ce thread dédié (Queue est intrinsèquement threadsafe) .

Ici, nous faisons un idqueue avec une longueur de seulement 2 (nous ne voulons pas la génération id pour se déchaîner, en faisant beaucoup de ids au préalable, ce qui gaspille la mémoire et épuise le réservoir d'entropie - pas sûr si 2 est optimal, mais le sweet spot va certainement être un petit entier ;-), donc le thread du générateur d'id va se bloquer en essayant d'ajouter le troisième, et attendre que de l'espace s'ouvre dans la file d'attente. idgetter (qui peut aussi être simplement défini par une affectation de niveau supérieur, idgetter = idqueue.get) trouvera normalement un identifiant déjà là et attendra (et fera de la place pour le prochain!) - sinon, il bloque et attend, se réveillant Dès que le générateur d'ID a placé un nouvel identifiant dans la file d'attente.

+0

Est-ce que idqueue.get() est accessible par tous les WSGIDaemonProcesses? –

+1

@Gert, si vous avez plusieurs processus au lieu de threads, vous pouvez faire la même chose avec un 'multiprocessing.Queue' au lieu d'un' Queue.Queue' (ce dernier est destiné à l'enfilage, pas au multitraitement). –

+0

Mod wsgi est un serveur wsgi multitraitement et multithread, comment puis-je utiliser les deux? –

0

N'existe-t-il pas une donnée unique dans chaque fil? Il m'est difficile d'imaginer deux threads avec exactement les mêmes données. Bien que je ne néglige pas la possibilité.

Dans le passé, quand j'ai fait des choses de cette nature, il y a habituellement quelque chose d'unique dans le sujet. Nom d'utilisateur ou nom du client ou quelque chose de cette nature. La solution pour moi était de concaténer le UserName, par exemple, et l'heure actuelle en millisecondes puis hachage cette chaîne et obtenir un condensé hexadécimal du hachage. Cela donne une belle chaîne qui a toujours la même longueur.

Il existe une possibilité très lointaine que deux types différents de John Smith (ou autre) dans deux threads génèrent l'ID dans la même milliseconde. Si cette possibilité rend nerveux, alors la voie de verrouillage mentionnée peut être nécessaire.

Comme déjà mentionné, il existe déjà des routines pour obtenir un GUID. Personnellement, j'aime bien jouer avec les fonctions de hachage. J'ai donc réussi à faire rouler les miennes de la manière indiquée sur les grands systèmes multi-threads. C'est en dernier ressort à vous de décider si vous avez vraiment des threads avec des données en double. Assurez-vous de choisir un bon algorithme de hachage. J'ai utilisé md5 avec succès mais j'ai lu qu'il est possible de générer une collision de hachage avec md5 bien que je ne l'ai jamais fait. Dernièrement, j'ai utilisé sha1.

3

Je suggère juste une petite modification de la réponse acceptée par Denis:

for i in range(MAX_ATTEMPTS): 
    sid = os.urandom(8).decode('hex') 
    try: 
     db.execute('INSERT INTO sessions (sid) VALUES (?)', (sid,)) 
    except IntegrityError: 
     continue 
    break 
else: 
    raise RuntimeError('Failed to generate unique session ID') 

Nous tenter simplement l'insert sans vérifier explicitement l'ID généré. L'insertion très échouera rarement, donc nous avons le plus souvent seulement à faire l'appel de la base de données, au lieu de deux. Cela permettra d'améliorer l'efficacité en réduisant le nombre d'appels à la base de données, sans compromettre la sécurité des threads (car le moteur de la base de données traitera ce problème efficacement).

+0

Cette solution entraînera des problèmes avec les transactions dans les cas graves. –

+0

Pouvez-vous expliquer ce qui doit exactement se passer pour que cela échoue? Qu'est-ce que l'autre thread doit faire alors que ce thread est sur le point d'avoir une exception? –

+0

Je ne peux pas vraiment voir dans quel cas cette solution aura plus de problèmes, mais il se peut que je manque quelque chose. Peux-tu élaborer? En fait, je ne vois aucune solution serait tout à fait approprié avec les transactions. Pour les transactions, je suggère que le moteur de base de données génère l'UID. Voir par exemple http://dev.mysql.com/doc/refman/5.1/en/miscellaneous-functions.html#function_uuid – edvald

0

mkdtemp doit être thread-safe, simple et sécurisé:

def uuid(): 
    import tempfile,os 
    _tdir = tempfile.mkdtemp(prefix='uuid_') 
    _uuid = os.path.basename(_tdir) 
    os.rmdir(_tdir) 
    return _uuid 
+0

Serait-ce pas trop l'utilisation du disque pour les identifiants de session? –

+0

Non, voir os.rmdir avant le retour. Quelqu'un doit faire le travail .. c'est juste l'OS du noyau dans mon exemple. Toute autre implémentation comme DB, etc. ajoutera des couches en plus de cela. En tout cas je pensais à la gestion de session où un répertoire est nécessaire. – Luca

+0

Comment savez-vous si _uuid est unique lorsque vous le supprimez? –