2017-09-15 2 views
3

Je récupère des milliers d'entités à partir d'une API une à la fois en utilisant des requêtes http. Comme la prochaine étape dans le pipeline, je veux tous les ramasser dans une base de données.Erreur idiomatique/gestion des exceptions avec des macros de threads

(->> ids 
    (pmap fetch-entity) 
    (pmap store-entity) 
    (doall)) 

fetch-entity attend un identifiant String et tente de récupérer une entité à l'aide d'une requête HTTP et renvoie soit un Map ou déclenche une exception (par exemple en raison d'un délai d'attente).

store-entity attend un Map et tente de le stocker dans une base de données. Il peut éventuellement générer une exception (par exemple, si le Map ne correspond pas au schéma de base de données ou s'il n'a pas reçu de Map).

erreur inélégant Handling

Ma première « solution » est d'écrire des fonctions wrapper fetch-entity' et store-entity' pour intercepter des exceptions de leurs fonctions originales respectives.

fetch-entity' renvoie son entrée en cas d'échec, en passant essentiellement un String id si la demande http a échoué. Cela garantit que l'ensemble du pipeline conserve le camionnage.

store-entity' vérifie le type de son argument. Si l'argument est Map (l'entité d'extraction a réussi et a renvoyé un Map), elle tente de le stocker dans la base de données.

Si la tentative de stockage de la base de données génère une exception ou si store-entity' a obtenu un passé String (id) au lieu d'un Map il sera conj à un Vector externe de error_ids.

De cette façon, je peux plus tard utiliser error_ids pour savoir combien de fois il y avait un échec et quels identifiants ont été affectés.

Il ne me semble pas que ce qui précède est un moyen sensé de réaliser ce que j'essaie de faire. Par exemple, la façon dont j'ai écrit store-entity' complique la fonction avec l'étape précédente du pipeline (fetch-entity') car elle se comporte différemment selon que l'étape précédente du pipeline a réussi ou non.

Egalement ayant store-entity' être conscient d'un Vector externe appelé error_ids ne se sent pas du tout. Existe-t-il une manière idiomatique de gérer ce genre de situations où vous avez plusieurs étapes de pipeline où certaines d'entre elles peuvent lancer des exceptions (par exemple parce qu'elles sont des E/S) où je ne peux pas facilement utiliser les prédicats se comportera de manière prévisible et où je ne veux pas déranger le pipeline et seulement vérifier plus tard dans quels cas il a mal tourné?

+1

Avez-vous regardé: http s: //github.com/adambard/failjure? –

Répondre

4

Il est possible d'utiliser un type de Try monade, par exemple de la cats library:

représente un calcul qui peuvent soit se traduire par une exception ou retourner une valeur calculée avec succès. Est très similaire à la monade, mais est sémantiquement différent.

Il se compose de deux types: succès et échec. Le type Success est un wrapper simple, comme Right of the Soit monad. Mais le type Failure est légèrement différent de Left, car il enveloppe toujours une instance de Throwable (ou n'importe quelle valeur dans cljs puisque vous pouvez lancer des valeurs arbitraires dans l'hôte JavaScript).



Il s'agit d'un analogue du bloc try-catch: il remplace le traitement des erreurs basé sur la pile de try-catch avec traitement des erreurs basé sur le tas. Au lieu d'avoir une exception levée et d'avoir à la traiter immédiatement dans le même thread, elle déconnecte la gestion des erreurs et la récupération.

La gestion des erreurs basée sur les tas est ce que vous voulez. Ci-dessous j'ai fait un exemple de fetch-entity et store-entity. J'ai fait fetch-entity jeter un ExceptionInfo sur le premier id (1) et store-entity jette un DivideByZeroException sur le deuxième id (0).

(ns your-project.core 
    (:require [cats.core :as cats] 
      [cats.monad.exception :as exc])) 


(def ids [1 0 2]) ;; `fetch-entity` throws on 1, `store-entity` on 0, 2 works 


(defn fetch-entity 
    "Throws an exception when the id is 1..." 
    [id] 
    (if (= id 1) 
    (throw (ex-info "id is 1, help!" {:id id})) 
    id)) 


(defn store-entity 
    "Unfortunately this function still needs to be aware that it receives a Try. 
    It throws a `DivideByZeroException` when the id is 0" 
    [id-try] 
    (if (exc/success? id-try)     ; was the previous step a success? 
    (exc/try-on (/ 1 (exc/extract id-try))) ; if so: extract, apply fn, and rewrap 
    id-try))        ; else return original for later processing 


(def results 
    (->> ids 
     (pmap #(exc/try-on (fetch-entity %))) 
     (pmap store-entity))) 

Maintenant, vous pouvez filtrer results sur les succès ou les échecs avec respectivement success? ou failure? et récupérer les valeurs via cats-extract

(def successful-results 
    (->> results 
     (filter exc/success?) 
     (mapv cats/extract))) 

successful-results ;; => [1/2] 


(def error-messages 
    (->> results 
     (filter exc/failure?) 
     (mapv cats/extract) ; gets exceptions without raising them 
     (mapv #(.getMessage %)))) 

error-messages ;; => ["id is 1, help!" "Divide by zero"] 

Notez que si vous voulez faire une boucle seulement sur la errors ou successful-results une fois que vous pouvez utiliser un transducteur comme suit:

(transduce (comp 
      (filter exc/success?) 
      (map cats/extract)) 
      conj 
      results)) 
;; => [1/2] 
1

Ma première pensée est de combiner fetch-entity et store-entity en une seule opération:

(defn fetch-and-store [id] 
    (try 
    (store-entity (fetch-entity id)) 
    (catch ... <log error msg>))) 

(doall (pmap fetch-and-store ids)) 

Would quelque chose comme ce travail?