Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TECH] Ajoute la domainTransaction dans le AsyncLocalStorage (PIX-13257). #9423

Merged

Conversation

frinyvonnick
Copy link
Member

@frinyvonnick frinyvonnick commented Jul 2, 2024

🦄 Problème

Le fonctionnement de la méthode execute de DomainTransaction oblige à passer à chaque méthode de repositories appelés dans les usecases à prendre en paramètre une instance de DomainTransaction.

Le problème de cette implémentation est qu'il arrive d'oublier de passer une domainTransaction et la valeur par défaut mise au niveau des méthodes repositories avec la méthode static emptyTransaction pour simplifier les tests cache ces oublis. La conséquence est que nous avons des usecases semi-transactionnels.

🤖 Proposition

On change le fonctionnement de la méthode execute pour qu'il stocke l'instance de la DomainTransaction dans le AsyncLocalStorage.

On ajoute un méthode static getConnection qui permet de récupérer une transaction si elle existe ou l'instance knex.

On conserve la possibilité d'utiliser les DomainTransaction tel qu'on le fait aujourd'hui pour permettre de migrer progressivement.

🌈 Remarques

Voici le différence d'utilisation avant et après.

Avant :

Usecase :

const updateCampaignCode = async function ({ campaignId, campaignCode, campaignAdministrationRepository }) {
  await DomainTransaction.execute(async (domainTransaction) => {
    const campaign = await campaignAdministrationRepository.get(campaignId, domainTransaction);
    if (!campaign) {
      throw new UnknownCampaignId();
    }

    campaign.updateFields({ code: campaignCode });

    const isCodeAvailable = await campaignAdministrationRepository.isCodeAvailable({
      code: campaignCode,
      domainTransaction,
    });
    if (!isCodeAvailable) {
      throw new CampaignUniqueCodeError();
    }

    await campaignAdministrationRepository.update(campaign, domainTransaction);
  });
};

Repository :

const get = async function (id, domainTransaction = DomainTransaction.emptyTransaction()) {
  const knexConn = domainTransaction.knexTransaction || knex;
  const campaign = await knexConn('campaigns').where({ id }).first();

  if (!campaign) return null;

  const { count: participationCount } = await knexConn('campaign-participations')
    .count('id')
    .where({ campaignId: id })
    .first();

  return new Campaign({
    ...campaign,
    participationCount,
  });
};

Apres

Usecase :

const updateCampaignCode = async function ({ campaignId, campaignCode, campaignAdministrationRepository }) {
-  await DomainTransaction.execute(async (domainTransaction) => {
+  await DomainTransaction.execute(async () => {
-    const campaign = await campaignAdministrationRepository.get(campaignId, domainTransaction);
+    const campaign = await campaignAdministrationRepository.get(campaignId);
    if (!campaign) {
      throw new UnknownCampaignId();
    }

    campaign.updateFields({ code: campaignCode });

-    const isCodeAvailable = await campaignAdministrationRepository.isCodeAvailable({
-      code: campaignCode,
-      domainTransaction,
-    });
+    const isCodeAvailable = await campaignAdministrationRepository.isCodeAvailable({ code: campaignCode });
    if (!isCodeAvailable) {
      throw new CampaignUniqueCodeError();
    }

-    await campaignAdministrationRepository.update(campaign, domainTransaction);
+    await campaignAdministrationRepository.update(campaign);
  });
};

Repository :

- const get = async function (id, domainTransaction = DomainTransaction.emptyTransaction()) {
+ const get = async function (id) {
-  const knexConn = domainTransaction.knexTransaction || knex;
+  const knexConn = DomainTransaction.getConnection();
  const campaign = await knexConn('campaigns').where({ id }).first();

  if (!campaign) return null;

  const { count: participationCount } = await knexConn('campaign-participations')
    .count('id')
    .where({ campaignId: id })
    .first();

  return new Campaign({
    ...campaign,
    participationCount,
  });
};

TODO : @1024pix/team-prescription va devoir supprimer leur expérimentation avec l'ApplicationTransaction en faveur de la DomainTransaction.

💯 Pour tester

La CI est verte 💚

@frinyvonnick frinyvonnick self-assigned this Jul 2, 2024
@pix-bot-github
Copy link

Une fois les applications déployées, elles seront accessibles via les liens suivants :

Les variables d'environnement seront accessibles via les liens suivants :

@frinyvonnick frinyvonnick added 👀 Tech Review Needed cross-team Toutes les équipes de dev labels Jul 2, 2024
@frinyvonnick frinyvonnick force-pushed the tech-make-domain-transaction-use-async-storage branch from 761f5c9 to 2eea3f8 Compare July 2, 2024 14:26
@frinyvonnick frinyvonnick marked this pull request as ready for review July 2, 2024 14:59
@frinyvonnick frinyvonnick requested review from a team as code owners July 2, 2024 14:59
@frinyvonnick frinyvonnick changed the title [TECH] Ajoute la transaction dans le AsyncLocalStorage. [TECH] Ajoute la domainTransaction dans le AsyncLocalStorage. Jul 2, 2024
Copy link
Contributor

@Steph0 Steph0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

J'aime le système à la ThreadLocal (s'affranchir de trimballer des parametres dans toutes les couches) !

ℹ️ Les parties barrées challengent la vision future, qui n'est pas celle que mets en place cette PR ! Ne pas en tenir compte !

Je pense qu'il faut vraiment challenger avec des connaisseurs en BDD ce nouveau système si on garde l'idée de créer à chaque requête une transaction par défaut.

Si je suis complètement en phase sur le système à la ThreadLocal (s'affranchir de trimballer des parametres dans toutes les couches), je suis + dubitatif sur le fait d'imposer une transaction systématique.

Est-ce que l'on veut pallier un manque de connaissances des développeurs (quand je dois faire un transaction, a quoi ça sert, etc.) en leur créant magiquement la chose par défaut ?

Par ailleurs, il faudrait vérifier en amont de la PROD les impacts sur l'infra, car dans un contexte de volumétrie ça peut générer des choses dures à voir sur une faible volumétrie (overhead, deadlock)

⚠️ je ne suis pas un expert BDD, je puise là dans mon vécu et des connaissances pratiques, en revanche je suis très intéressé par étudier les points soulevés ici avec un sachant :)

@frinyvonnick
Copy link
Member Author

Sur une suggestion de @HEYGUL, j'ai rajouté un commit qui introduit un nouvel outil pour manipuler les transactions. C'est une higher order function qui permet d'améliorer la DX d'utilisation des domain transactions au niveau du usecase.

Avant :

import { DomainTransaction } from '../../../../shared/domain/DomainTransaction.js';
import { CampaignUniqueCodeError, UnknownCampaignId } from './../errors.js';

const updateCampaignCode = async function ({ campaignId, campaignCode, campaignAdministrationRepository }) {
  await DomainTransaction.execute(async (domainTransaction) => {
    const campaign = await campaignAdministrationRepository.get(campaignId, domainTransaction);
    if (!campaign) {
      throw new UnknownCampaignId();
    }

    campaign.updateFields({ code: campaignCode });

    const isCodeAvailable = await campaignAdministrationRepository.isCodeAvailable({
      code: campaignCode,
      domainTransaction,
    });
    if (!isCodeAvailable) {
      throw new CampaignUniqueCodeError();
    }

    await campaignAdministrationRepository.update(campaign, domainTransaction);
  });
};

Après :

import { transaction } from '../../../../shared/domain/DomainTransaction.js';
import { CampaignUniqueCodeError, UnknownCampaignId } from './../errors.js';

const updateCampaignCode = transaction(async function ({ campaignId, campaignCode, campaignAdministrationRepository }) {
  const campaign = await campaignAdministrationRepository.get(campaignId);
  if (!campaign) {
    throw new UnknownCampaignId();
  }

  campaign.updateFields({ code: campaignCode });

  const isCodeAvailable = await campaignAdministrationRepository.isCodeAvailable({ code: campaignCode });
  if (!isCodeAvailable) {
    throw new CampaignUniqueCodeError();
  }

  await campaignAdministrationRepository.update(campaign);

});

Sur le nommage, nous avons un doute avec @HEYGUL qui proposait aussi embedInTransaction. On va faire un vote sur Slack 🤗

@frinyvonnick frinyvonnick changed the title [TECH] Ajoute la domainTransaction dans le AsyncLocalStorage. [TECH] Ajoute la domainTransaction dans le AsyncLocalStorage (PIX-13257). Jul 3, 2024
@dlahaye
Copy link
Contributor

dlahaye commented Jul 3, 2024

Sur une suggestion de @HEYGUL, j'ai rajouté un commit qui introduit un nouvel outil pour manipuler les transactions. C'est une higher order function qui permet d'améliorer la DX d'utilisation des domain transactions au niveau du usecase.

@frinyvonnick je découvre donc aujourd'hui qu'on a un DomainTransaction dans notre "couche" domaine avec du knex importé dedans 😨 Comment a été décidé cette entorse à l'inversion de dépendance ? (une petite entorse soit mais entorse quand même)

@frinyvonnick
Copy link
Member Author

frinyvonnick commented Jul 3, 2024

Sur une suggestion de @HEYGUL, j'ai rajouté un commit qui introduit un nouvel outil pour manipuler les transactions. C'est une higher order function qui permet d'améliorer la DX d'utilisation des domain transactions au niveau du usecase.

@frinyvonnick je découvre donc aujourd'hui qu'on a un DomainTransaction dans notre "couche" domaine avec du knex importé dedans 😨 Comment a été décidé cette entorse à l'inversion de dépendance ? (une petite entorse soit mais entorse quand même)

Je vois que @dimitrilahaye tu partages mon malaise sur la question. Après discussion avec @bpetetot et @HEYGUL voici ce qui en ressort :

  • La notion de transaction fonctionnelle existe peu importe l'outil de persistence utilisé derrière, c'est pour ça qu'on le cache dans une classe DomainTransaction
  • Jusqu'à maintenant côté Prescription, on utilisait les DomainTransaction au niveau du controller à cause de cette entorse. Cependant, les usecase ne sont pas 1 pour 1 avec controller. Par exemple, le usecase de suppression des prescrits va être utilisé par l'API mais aussi par deux évènements (anonymisation des utilisateurs, archivage des organisations). Ce qui fait qu'on doit bien penser à chaque fois à bien mettre une transaction vs mettre dans la transaction dans le usecase et c'est fait une bonne fois pour toute.

Je laisse les autres compléter 😄

EDIT :
L'adr sur le sujet est ici : https://github.com/1024pix/pix/blob/dev/docs/adr/0009-transaction-metier.md

Copy link
Member

@VincentHardouin VincentHardouin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM c'est cool 🌮

Il manque à mon sens de mettre à jour / amender l'ADR n°9 / n° 25 avec ce nouveau choix

@pix-service-auto-merge pix-service-auto-merge force-pushed the tech-make-domain-transaction-use-async-storage branch from 4fbbdc6 to 726a519 Compare July 4, 2024 12:30
@pix-service-auto-merge pix-service-auto-merge merged commit 295f2b2 into dev Jul 4, 2024
6 of 7 checks passed
@pix-service-auto-merge pix-service-auto-merge deleted the tech-make-domain-transaction-use-async-storage branch July 4, 2024 12:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants