odavid/typeorm-transactional-cls-hooked

How to use Hook?

KevinTanjung opened this issue · 4 comments

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?

mentioning @svvac

svvac commented

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

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

Alright, looks good to me. 👍