Celery: Fonctionnalité : Beat doit éviter les appels simultanés

Créé le 17 nov. 2010  ·  48Commentaires  ·  Source: celery/celery

Exiger de l'utilisateur qu'il s'assure qu'une seule instance de celerybeat existe dans son cluster crée une charge de mise en œuvre importante (soit en créant un point de défaillance unique, soit en encourageant les utilisateurs à déployer leur propre mutex distribué).

celerybeat devrait soit fournir un mécanisme pour empêcher la concurrence par inadvertance, soit la documentation devrait suggérer une approche de meilleure pratique.

Celerybeat

Commentaire le plus utile

@ankur11 single-beat garantit qu'une seule instance de celery beat est en cours d'exécution, mais ne synchronise pas l'état de la planification entre les instances.

Si j'utilisais le planificateur par défaut avec une tâche périodique destinée à s'exécuter toutes les 15 minutes et que j'avais un basculement avec un seul battement 14 minutes après la dernière exécution de la tâche, la tâche ne s'exécuterait que 15 minutes après le nouveau battement de céleri instance a commencé, ce qui a entraîné un écart de 29 minutes.

Pour partager l'état de la planification entre les instances, j'avais besoin d'utiliser un autre planificateur . django-celery-beat est l'alternative mentionnée dans les documents Celery, mais ma préférence était d'utiliser Redis comme backend pour la synchronisation de planification, car j'utilisais déjà Redis comme backend Celery.

Redbeat inclut à la fois l'état de planification partagé soutenu par Redis et le verrouillage pour garantir qu'une seule instance planifie les tâches. Je n'ai donc pas eu besoin d'un seul temps ou de BeatCop une fois que j'ai commencé à l'utiliser.

Dans notre implémentation, celery beat est démarré par superviseur sur toutes les instances, avec Redbeat comme planificateur (par exemple exec celery beat --scheduler=redbeat.RedBeatScheduler --app=myproject.celery:app ). Malheureusement, je ne peux pas partager de code lié au travail, mais je suis heureux de répondre à toute question supplémentaire sur l'implémentation en général.

Tous les 48 commentaires

Cela pourrait être résolu en utilisant kombu.pidbox , c'est aussi ainsi que celeryd détecte qu'il y a déjà un nœud avec le même nom en cours d'exécution. Puisque celerybeat est centralisé, il pourrait utiliser un nom de nœud fixe.

Comme effet secondaire, nous pourrons contrôler celerybeat avec des commandes de télécommande (il pourrait y avoir une commande pour recharger le calendrier par exemple, ou voir quelles tâches sont dues dans un proche avenir). C'est un effet secondaire assez impressionnant si vous me demandez.

Nécessite plus de planification, car il existe un cas d'utilisation pour exécuter plusieurs instances dans le même cluster. Par exemple, pour "sharding" le planning en plusieurs morceaux. Il doit y avoir au moins la possibilité de sélectionner un nom de nœud pour chaque instance. Report à 2.3.0.

Nous avons eu un problème où la boîte exécutant celerybeat s'est déconnectée sans une bonne solution de repli pour démarrer une nouvelle instance de celerybeat pour prendre sa place. Quelle est la méthode HA recommandée pour exécuter celerybeat ?

L'approche kombu.pidbox nous permettrait-elle d'exécuter plusieurs instances de celerybeat qui se mettraient en veille si elle détectait qu'une instance était déjà en cours d'exécution avec le nom de nœud fixe et s'interrogerait pour devenir active si cela l'instance tombe en panne ?

L'exécution de plusieurs instances actives semble intéressante - quels autres avantages pourrait-il y avoir en plus du partage du calendrier ?

+1

+1

C'est quelque chose qui est une réelle préoccupation pour les grands déploiements où la résilience de la planification est importante.

+9999 ;)
Y a-t-il un problème avec l'utilisation kombu.pidbox solution

Pidbox pourrait être utilisé, mais le problème est que beat n'est pas un consommateur. Pour répondre aux messages diffusés comme « des instances de battement ici ? » il devrait constamment écouter les messages sur sa file d'attente de diffusion, et il ne le peut actuellement pas car il est occupé à planifier les messages.

Techniquement, il pourrait utiliser un deuxième thread, mais cela peut réduire les performances et représente beaucoup de frais généraux uniquement pour cette fonctionnalité.

Une deuxième solution pourrait être d'utiliser un verrou, mais avec l'inconvénient de devoir le déverrouiller. C'est-à-dire que si le processus de battement est tué, le verrou périmé nécessiterait une intervention manuelle pour démarrer une nouvelle instance.

Il pourrait également avoir un délai d'attente de 2 secondes sur le verrou et mettre à jour le verrou toutes les secondes. Cela signifie alors qu'une nouvelle instance devra attendre 2 secondes si le verrou est maintenu.

Un verrou dans amqp pourrait être créé en déclarant une file d'attente, par exemple `queue_declare('celerybeat.lock', arguments={'x-expires': 2000}``

+1

j'aimerais bien voir ça

+1

+1

+1 aussi

+1

Quelqu'un a-t-il réellement mis en œuvre la solution kombu.pidbox ou tout autre mécanisme qui résout ce problème ? Si oui, merci de le partager. Il y a encore beaucoup de gens qui se demandent quelle est la meilleure pratique.

Quelqu'un a-t-il complètement abandonné le céleri à cause de cela? Je serais également intéressé de le savoir.

ÉDITER:

J'ai trouvé cet essentiel (https://gist.github.com/winhamwr/2719812) via une discussion google (https://www.google.co.in/search?q=celerybeat+lock&aq=f&oq=celerybeat+lock&aqs= chrome.0.57j62l3.2125j0&sourceid=chrome&ie=UTF-8).

Je me demande également si quelqu'un vient d'utiliser un fichier pid partagé pour celerybeat directement, peut-être avec un EBS sur AWS ou peut-être dans un compartiment S3… celerybeat --pidfile=/path/to/shared/volume .

J'ai remarqué que le maître actuel (3.1 dev) a une étape de potins pour le consommateur. Serait-il possible de tirer parti de la file d'attente des potins et de l'élection du leader pour coordonner les processus de battement intégrés ? C'est-à-dire que chaque travailleur exécuterait le processus de battement intégré, mais seul le leader mettrait en file d'attente la tâche périodique. Cela supposerait probablement un stockage de planification partagé.

@mlavin Cela pourrait fonctionner, mais uniquement pour les transports de courtier qui prennent en charge la diffusion

Le problème avec la solution pidbox est que le programme celerybeat doit ensuite être réécrit pour utiliser les E/S Async.
Il ne peut actuellement pas à la fois consommer des tâches et les produire, car le planificateur est bloquant.

Dans la plupart des cas, cette fonctionnalité n'est pas du tout nécessaire, car la plupart des déploiements de production ont un hôte dédié pour le processus de battement, et l'utilisation d'un --pidfile suffit pour s'assurer que vous ne démarrez pas plusieurs instances.

J'ai constaté que souvent les personnes affectées par ce problème sont celles qui utilisent l'option -B dans une démonisation
script, puis duplique cette configuration sur un autre hôte.

Donc je comprends que c'est ennuyeux, mais je ne pense pas que ce soit critique. Si quelqu'un veut vraiment une solution, il peut y contribuer, ou m'engager/faire un don pour la mettre en œuvre.

On peut utiliser uWSGI pour avoir un processus à un seul battement avec repli vers d'autres nœuds

+1, nous lançons des instances Amazon EC2 identiques et ce sera bien d'avoir des tâches périodiques qui ne s'exécutent que dans un seul nœud. En attendant, je vais essayer d'utiliser uWSGI merci pour la suggestion.

+1

+1

Au travail, j'ai plaidé pour l'utilisation de Celerybeat pour la planification, mais le fait de ne pas disposer de HA prêt à l'emploi rend la tâche très difficile. En fait, il semble que nous allons l'abandonner complètement à cause de cela. Tout simplement, l'exécution d'une seule instance Celerybeat en fait un point de défaillance unique et n'est donc pas prête pour la production.

@junaidch Je ne pense pas que vous devriez laisser tomber le céleri à cause de cela. Vous pouvez toujours simplement exécuter le planificateur sur chaque serveur et pour les tâches périodiques, utilisez une sorte de mécanisme de verrouillage pour vous assurer qu'elles ne se chevauchent en aucune façon et ne s'exécutent pas trop souvent. De plus, vous pouvez sous-classer le planificateur et y verrouiller également ou ignorer le verrouillage au niveau des tâches et tout faire dans le planificateur à la place.

Ce serait mieux d'avoir des fonctionnalités intégrées dans le céleri car c'est une sorte de solution de contournement, mais c'est quand même utilisable en production très bien.

Merci @23doors.

Mes tâches maintiennent déjà un verrou Redis pour empêcher l'exécution d'une autre instance de la tâche. Si je lance 2 battements sur 2 machines différentes et que mes tâches sont programmées à des intervalles de 5 minutes, je pense que cela fonctionnera même si les deux battements pousseront les tâches dans la file d'attente. Il est encore plus difficile de plaider en faveur de l'adoption lorsque vous devez mettre en œuvre une solution de contournement pour vos fonctionnalités principales.

Je vais étudier la recommandation de sous-classement. Cela pourrait être une approche plus propre.

Merci pour les suggestions !

Chez Lulu, nous avons résolu ce problème en écrivant un simple gestionnaire de singleton de cluster (nommé BeatCop). Il utilise un verrou Redis expirant pour s'assurer qu'il n'y a qu'un seul Celerybeat en cours d'exécution dans un pool d'autoscaling de travailleurs Celery. Si quelque chose arrive à ce Celerybeat (comme l'instance est mise à l'échelle ou meurt ou Celerybeat se bloque), un autre nœud génère automatiquement un nouveau Celerybeat. Nous avons open source BeatCop .

@ingmar nous avons écrit ceci https://github.com/ybrs/single-beat pour les mêmes raisons, la dernière fois que j'ai vérifié, je n'ai pas vu votre commentaire. nous avons également publié en tant qu'opensource qui pourrait être utile à d'autres. fait plus ou moins la même chose.

pour autant que je sache, les principales différences avec beatcop -, nous utilisons pyuv - donc beatcop est plus portable, je pense moins de dépendances -, redirigez les enfants stderr et stdout en tant que parents, et quittez si l'enfant meurt avec le même code, configurez-le avec variables d'environnement. donc c'est un peu plus facile à ajouter au superviseur.

j'espère que cela pourra être utile à quelqu'un d'autre.

+1

+1

J'envisage d'utiliser les valeurs-clés de Consul, en tant que contrôleur de verrouillage, quelqu'un a-t-il essayé cette approche ? Ainsi, tant qu'une instance fonctionne, les autres "s'endormiraient" jusqu'à ce que le verrou ne soit pas mis à jour, puis le mécanisme d'élection du consul déciderait qui serait celui qui mettrait à jour la valeur de la clé horodatée. Celui qui met à jour le verrou est celui qui travaille.

@ingmar Merci pour ça ! Je vais essayer ceci sur mon cluster de travailleurs.

+10 car la mise en œuvre actuelle signifie un point de défaillance unique qui explique pourquoi nous utilisons une file d'attente distribuée en premier lieu

+1

+1

On dirait que cela va être dans la v5.0.0 https://github.com/celery/celery/milestones/v5.0.0

+1

Pour terminer, comme avec les ressources actuelles, il faudra 10 ans pour terminer.

Désolé, mais c'est un problème sérieux pour une file d'attente dite "distribuée". Quel que soit le temps que cela prendra à mettre en œuvre, cela devrait éventuellement être corrigé. Fermer un problème parfaitement valide parce que vous n'avez pas les ressources _en ce moment_ ne semble pas correct. Pourriez-vous peut-être le rouvrir et appliquer une étiquette indiquant qu'il est de faible priorité pour le moment ?

Je sais que la raison de ma fermeture était absurdement abrupte, donc en tant qu'utilisateur de logiciel, je peux comprendre votre sentiment, mais techniquement, Beat ressemble plus à une fonctionnalité complémentaire. Il est complètement découplé du reste de Celery, et il a été intentionnellement conçu pour être non distribué afin de simplifier la mise en œuvre. Cela a commencé comme un moyen judicieux de définir les tâches cron de Python en tant que bonus pour les utilisateurs utilisant déjà Celery, puis de plus en plus de personnes ont utilisé Celery en remplacement de cron.

La question est ouverte depuis SIX ans maintenant, et même si elle est souvent demandée et que d'innombrables entreprises en dépendent, aucune n'a jamais proposé de payer pour sa mise en œuvre.

C'était en fait l'une des questions que j'ai pensé qu'il serait intéressant pour les entreprises de parrainer. Certes, il n'est pas courant que les entreprises proposent de payer pour une fonctionnalité, une correction de bogue ou même pour aider à résoudre un problème de production. Je peux probablement les compter sur une main (vous êtes génial), alors maintenant je sais à quel point cette idée était naïve :)

J'ai également fermé un duplicata de ce problème aujourd'hui, voir #1495. Il y a eu des demandes d'extraction essayant de résoudre le problème, et plusieurs sont prometteuses, mais étant donné le dévouement requis pour prouver qu'une implémentation donnée fonctionne, je n'ai toujours pas eu le temps de les examiner correctement. Peut-être que cela pousse quelqu'un à agir, même si ce n'est pas le cas, je pense que c'est mieux que de garder une demande de fonctionnalité ouverte pendant six ans, quand personne ne travaille dessus. C'est aussi une sorte de service rendu aux utilisateurs qui souhaitent que cela soit corrigé.

@ask Assez juste. Il est vrai que cron distribué est un gros problème compliqué, comme vous le dites dans l'autre fil. Et cela ressemble à quelque chose qui devrait vivre en dehors de Celery.

Merci d'avoir pris le temps de détailler votre raisonnement.

@ask Je me demandais si ce problème pouvait être contourné en localisant le fichier celerybeat-schedule (utilisé par celery.beat.PersistentScheduler ) dans un volume NFS partagé entre tous les nœuds du cluster ?

La classe PersistentScheduler utilise shelve comme module de base de données, donc les écritures simultanées dans le fichier celerybeat-schedule devraient être empêchées par conception. Voici un extrait de la shelve documentation :

Le module shelve ne prend pas en charge l'accès simultané en lecture/écriture aux objets mis en étagère. (Plusieurs accès en lecture simultanés sont sûrs.) Lorsqu'un programme a une étagère ouverte pour l'écriture, aucun autre programme ne devrait l'avoir ouverte pour la lecture ou l'écriture.

En supposant que nous commencions à battre le céleri comme ceci :

celery -A project-name beat -l info -s /nfs_shared_volume/celerybeat-schedule

/nfs_shared_volume est le volume partagé (par exemple, géré par AWS Elastic File System), pouvons-nous nous attendre à ce que les planifications ne soient pas perturbées même s'il y a un processus de battement de céleri en cours d'exécution sur chaque nœud du cluster ?

@mikeschaekermann Si je lis correctement la documentation, shelve ne fait aucun effort pour empêcher l'accès en écriture simultané. Il vous dit simplement de ne pas laisser cela se produire. La section que vous avez citée poursuit en disant "Le verrouillage de fichier Unix peut être utilisé pour résoudre ce problème, mais cela diffère selon les versions d'Unix et nécessite des connaissances sur l'implémentation de la base de données utilisée."

@ze-phyr-us Je pense que vous avez raison, et j'ai mal interprété les shelve docs. Pourtant, je me demande si le problème serait résolu en supposant que le backend Scheduler assure les opérations @ask le package django-celery-beat en charge l'atomicité pour résoudre le problème ? J'ai vu qu'il utilise des transactions pour effectuer certaines des mises à jour.

Pour tous ceux qui se retrouvent ici à la recherche d'un rythme de céleri convivial distribué/auto-évolutif et qui sont heureux d'utiliser Redis comme backend ; J'ai essayé à la fois BeatCop et single-beat mentionnés ci-dessus, mais j'ai finalement choisi

Salut @ddevlin
J'ai des problèmes similaires, à quels problèmes avez-vous été confronté lors de l'utilisation d'un seul temps ? De plus, si ce n'est pas trop, pourriez-vous s'il vous plaît partager l'exemple d'implémentation de la façon dont vous avez configuré redbeat pour plusieurs serveurs.

@ankur11 single-beat garantit qu'une seule instance de celery beat est en cours d'exécution, mais ne synchronise pas l'état de la planification entre les instances.

Si j'utilisais le planificateur par défaut avec une tâche périodique destinée à s'exécuter toutes les 15 minutes et que j'avais un basculement avec un seul battement 14 minutes après la dernière exécution de la tâche, la tâche ne s'exécuterait que 15 minutes après le nouveau battement de céleri instance a commencé, ce qui a entraîné un écart de 29 minutes.

Pour partager l'état de la planification entre les instances, j'avais besoin d'utiliser un autre planificateur . django-celery-beat est l'alternative mentionnée dans les documents Celery, mais ma préférence était d'utiliser Redis comme backend pour la synchronisation de planification, car j'utilisais déjà Redis comme backend Celery.

Redbeat inclut à la fois l'état de planification partagé soutenu par Redis et le verrouillage pour garantir qu'une seule instance planifie les tâches. Je n'ai donc pas eu besoin d'un seul temps ou de BeatCop une fois que j'ai commencé à l'utiliser.

Dans notre implémentation, celery beat est démarré par superviseur sur toutes les instances, avec Redbeat comme planificateur (par exemple exec celery beat --scheduler=redbeat.RedBeatScheduler --app=myproject.celery:app ). Malheureusement, je ne peux pas partager de code lié au travail, mais je suis heureux de répondre à toute question supplémentaire sur l'implémentation en général.

@mikeschaekermann, vous pouvez essayer d'envelopper votre rythme de céleri avec /use/bin/flock pour verrouiller l'accès ...

flock /nfs/lock.file celery beat ...

En supposant que vous fassiez confiance à votre implémentation de verrouillage NFS :)

Cela garantirait qu'un seul fonctionne réellement et que les autres bloquent jusqu'à ce que le casier meure.

@mikeschaekermann, vous pouvez essayer d'envelopper votre rythme de céleri avec /use/bin/flock pour verrouiller l'accès ...

troupeau /nfs/lock.file battement de céleri ...

En supposant que vous fassiez confiance à votre implémentation de verrouillage NFS :)

Cela garantirait qu'un seul fonctionne réellement et que les autres bloquent jusqu'à ce que le casier meure.

J'ai essayé cette méthode. Malheureusement, si le client qui détient le verrou NFS perd la connectivité au serveur NFS, le verrou peut être révoqué par le serveur NFS et transmis à un autre client. Lorsque le détenteur du cadenas d'origine retrouve la connectivité, flock ne se rend pas compte que le cadenas a été révoqué. Il y a donc maintenant deux nœuds qui pensent qu'ils sont le « chef ».

J'ai fini par utiliser un verrou consultatif dans Postgres. J'ai créé une commande de gestion Django qui utilise le module django_pglocks et exécute celery beat dans un sous-processus.

J'ai fini par utiliser un verrou consultatif dans Postgres. J'ai créé une commande de gestion Django qui utilise le module django_pglocks et exécute celery beat dans un sous-processus.

Cela semble être sensible aux mêmes problèmes que j'ai vu avec l'utilisation de NFS. Que se passe-t-il si le client qui détient le verrou perd la connexion avec le serveur Postgres, ou si le serveur Postgres est redémarré ?

@swt2c Argh, bien sûr que tu as raison ! Il doit y avoir une sorte de maintien en vie.

En ce moment je fais :

def _pre_exec():
    prctl.set_pdeathsig(signal.SIGTERM)

with advisory_lock(LOCK_ID) as acquired:
            assert acquired
            logging.info("Lock acquired: %s", acquired)
            p = subprocess.Popen(
                celery,
                shell=False,
                close_fds=True,
                preexec_fn=_pre_exec,
            )
            sys.exit(p.wait())

advisor_lock prend en charge la récursivité, mais je ne sais pas s'il vérifie réellement la base de données :

In [8]:  with advisory_lock('foo') as acquired:
   ...:     print acquired
   ...:     while True:
   ...:        with advisory_lock('foo') as acquired:
   ...:           print acquired
   ...:        time.sleep(1)
   ...:       

# Yes, it does:

True
True
True
<shutdown the instsance>
InterfaceError: cursor already closed

Donc ... je pourrais le modifier pour continuer à sous-acquérir le verrouillage/l'interrogation et tuer le battement en cas d'échec. Ne garantit pas l'exclusion mutuelle, mais cela pourrait suffire à mes fins.

Dans mon cas, les battements simultanés sont une gêne inutile, mais pas un problème d'intégrité. Si c'était le cas, je pourrais également envelopper la tâche dans un verrou consultatif qui, si la base de données tombe en panne, échouera de toute façon.

J'ai également fait en sorte que le battement stocke le calendrier dans la base de données, mais je n'ai pas testé ce que fait le battement lorsque la base de données tombe en panne.

@ddevlin J'étais heureux de voir votre commentaire car c'était également la solution que je pensais mettre en œuvre.

Cependant, si vous pouviez partager la logique de la façon dont le superviseur redémarre automatiquement redbeat-1 lorsque redbeat-2 tombe en panne, ce serait d'une grande aide.

Cela peut être dû à mon manque de compréhension concernant supervisor , mais il semble que le autorestart=True soit efficace que pour les programmes qui entrent au moins une fois dans l'état RUNNING .

Mon problème est:

  1. J'ai deux program dans mon superviseur.conf de celery beat avec redbeat.RedBeatScheduler .
  2. Superviseur de départ, un beat ( beat-1 ) obtient le verrou et s'exécute, tandis que l'autre ( beat-2 ) essaie de démarrer plusieurs fois et entre dans le FATAL (avec l'erreur Seems we're already running? ).
  3. Idéalement, si beat-1 s'arrête, alors je veux que le superviseur démarre beat-2 .
  4. Cependant, cela ne se produit pas car il n'a jamais été dans un état RUNNING pour commencer. Ce qui signifie que si j'arrête beat-1 , ça s'arrête et rien ne se passe.

De mémoire, la solution serait d'avoir un cron qui continue de faire un supervisorctl restart all toutes les 5 secondes environ, mais je voulais juste avoir votre avis sur la façon dont vous avez pu atteindre cette redondance avec le superviseur.

Salut @harisibrahimkv , votre problème est que vous démarrez deux instances identiques de battement de céleri sur le même hôte; Je suppose que vous voyez ERROR: Pidfile (celerybeat.pid) already exists. dans vos journaux pour beat-2 ? Je peux voir que le fait d'avoir deux instances de céleri s'exécutant sur le même hôte serait utile pour tester le basculement entre elles, mais pour une réelle redondance, vous voulez probablement que céleri s'exécute sur plusieurs hôtes.

Pour que plusieurs instances s'exécutent sur le même hôte, demandez au superviseur de les démarrer avec l'argument --pidfile et donnez-leur des fichiers pid distincts : par exemple

# beat-1 
celery beat --scheduler=redbeat.RedBeatScheduler --pidfile="beat-1.pid" ...
# beat-2
celery beat --scheduler=redbeat.RedBeatScheduler --pidfile="beat-2.pid" ...

Les deux instances doivent démarrer avec succès sous superviseur, mais si vous vérifiez les fichiers journaux, une seule d'entre elles doit planifier des tâches. Si vous arrêtez cette instance, vous devriez voir l'autre instance prendre en charge la planification des tâches.

Notre objectif était d'avoir un pool de mise à l'échelle automatique d'hôtes identiques exécutant des ouvriers et des batteurs de céleri sous le superviseur. Chaque hôte a une seule instance de battement de céleri. Dans cette configuration, celery beat doit démarrer avec succès sur tous les hôtes, mais toutes les instances de celery beat qui n'acquièrent pas le verrou seront effectivement des secours automatiques et ne planifieront pas de tâches (bien que tous les hôtes du pool traiteront les tâches). Si l'instance avec le verrou est arrêtée (par exemple, lorsque le pool est réduit ou lorsque nous effectuons une mise à niveau progressive des hôtes du pool), l'une des instances de secours acquerra le verrou et prendra en charge les tâches de planification.

@ddevlin Merci beaucoup d'être revenu vers moi et d'avoir fait d'Internet un endroit si merveilleux ! Appréciez-le sincèrement ! (j'étais en train de parler de ta réponse à toute ma famille :D)

  1. Le bit pidfile fonctionné et j'étais tellement heureux de voir beat-2 reprendre les tâches lorsque l'autre s'est arrêté. Pourrait configurer le temps du battement avec CELERYBEAT_MAX_LOOP_INTERVAL = 25 (sur celery 3.x).

  2. Oui, pour une réelle redondance, nous prévoyons d'avoir cette configuration sur différentes instances. Merci d'avoir expliqué la configuration que vous utilisiez. Je vais travailler là-dessus maintenant. La configuration « plusieurs hôtes sur la même instance », comme vous l'avez bien compris, consistait simplement à valider initialement si le concept de basculement fonctionne avec cette configuration de superviseur.

Merci chaleureusement,
D'un petit village à l'extrême sud du sous-continent indien. :)

Cette page vous a été utile?
0 / 5 - 0 notes