odavid / typeorm-transactional-cls-hooked

A Transactional Method Decorator for typeorm that uses cls-hooked to handle and propagate transactions between different repositories and service methods. Inpired by Spring Trasnactional Annotation and Sequelize CLS
MIT License
524 stars 86 forks source link

How to use Hook? #31

Closed KevinTanjung closed 4 years ago

KevinTanjung commented 4 years ago

Hi @odavid!

First, I wanted to say thanks for the awesome library. Solve most of my use cases really well. Right now I am trying to figure out how to do a retry if a certain Transaction failed due to for example the Isolation Level of Postgres.

Let's say I have some service that does the following:

class Handler {
  @Transactional()
  async handle(data: any): Promise<void> {
    return new Promise(async (resolve, reject) => {
      let result;
      runOnTransactionEnd(async (err: Error | null) => {
        if (err) {
          // Retry
          try {
            result = await this._prepareData(data);
          } catch (err) {
            reject(err);
          }
        }
        await this._broadcastEvent(new OrderCreated(result));
        resolve(result);
      });
      result = await this._prepareData(data);
    });
  }

  async _prepareData(data: any) {
    const user = await this.userService.retrieveUser(data.userId);
    if (!user) {
      await this.userService.createUser(data.userId);
    }
    try {
      const order = await this.orderService.makeOrder(user, data);
      return { user, order };
    } catch (err) {
      this.logger.error(err);
      throw new Error('Something wrong, rollback!!');
    }
  }  
}

Let's assume I will only retry once, so if a certain error happens on my subsequent queries, due to for example concurrent update, I want to retry the query again with the updated result.

If no error, then I want to proceed to the broadcastEvent. But it seems that my Promise will be unhandled when I don't wrap the result with a try/catch, but if I wrapped it with a try/catch, the runOnTransactionEnd does not seem to be executed. Could you provide an example, on how for example you can achieve a retry?

KevinTanjung commented 4 years ago

mentioning @svvac

svvac commented 4 years ago

The hooks aren't really the place for your retry logic. It was actually more designed to perform your event broadcasting event.

I'd make OrderService.makeOrder() (responsible for the creation) do something like the following (that's no requirement though, you can still chose to do the same in Handler.handle()) :

@Transactional()
async makeOrder (user: User, data: IOrder): Promise<Order> {
     // validate stuff
    const order = await persist(data); // persist order here
    runOnTransactionCommit(() => this._broadcastEvent(new OrderCreated(order)));
    return order;
}

The handler is thus something like :


class Handler {
    @Transactional()
    async handle (data): Promise<void> {
        const user = await this.userService.retrieveUser(data.userId);
        if (!user) {
            await this.userService.createUser(data.userId);
        }

        try {
            const order = await this.orderService.makeOrder(user, data);
            return { user, order };
        } catch (err) {
            this.logger.error(err);
            throw new Error('Something wrong, rollback!!');
        }
    }

    async handleRetry (data, maxTries: number = 1): Promise<void> {
        let lastErr: Error = null;
        for (let i = 0; i < maxTries; i++) {
            try {
                return await this.handle(data);
            } catch (err) {
                lastErr = err;
            }
        }
        assert(lastErr !== null);
        throw last_err;
    }
}

EDIT : used wrong hook

odavid commented 4 years ago

I agree with @svvac - (sorry for the late response)

KevinTanjung commented 4 years ago

Alright, looks good to me. 👍