Promise 내부에서 선언한 setTImeout은 해당 promise가 소멸했더라도 살아있다. 타이머 클리어의 중요성!
명령을 두번 보낸다.
TImer( setTimeout )에서 조건만 보고 원하는 행위를 처리하도록 해놨다.
타이머를 클리어 하지 않았을 때 발생할 수 있는 레이스 컨디션? 문제
- 첫번째 await handleDispensing(); 명령은 이미 성공했고, this.resolver[operationCodeValue] = null로 초기화 하였다., 타이머를 클리어 하지 않아서 2초 뒤에 첫번째 명령어를 내릴 때 설정했던 타이머가 동작한다.
- 두번째 명령어가 동작하면서 다시 this.resolver[operationCodeValue] = resolver를 등록하였는데, 이 이후로 첫번째 명령어의 타이머가 돌면서 this.resolver[operationCodeValue] ! == null 이므로 첫번째 명령어의 타이머에서 타임아웃 처리를 하면서 this.resolver[operationCodeValue] = null; 로직이 돌게되어 resolver를 없애버렸다. 추가로, 첫번째 명령어에 대한 Promise는 이미 처리가 되었으므로 reject구문을 거치지만 실행되지 않으며, 예외를 띄우지 않는다.
- 두번째 명령어의 결과를 수신하지 못해서 타임아웃 처리를 하려고 하는데, 타임아웃 처리를 하는 로직이 this.resolver[operationCodeValue] ! == null 이어야 하는데, 첫번째 명령어의 타임아웃 처리로 인해서 null로 되면서 두번째 명령어의 타임아웃 처리가 되지 않는 문제가 있다.
private sendMessage(payload: number[]): void { const message = this.getMessageToSend(payload); logWebHook(`[전송메세지] : ${message}`); logRelease(`[전송메세지] : ${message}`); RNSerialport.writeHexString(message); } private async sendAsyncMessage( operationCodeValue: OperationCodeValue, payload: number[], ): Promise<number[]> { return new Promise<number[]>((resolve, reject) => { const sendAsyncMessage = (): void => { // 토출명령은 CHECK 명령 시에는 ACQ를 별도로 수신한다. if ( operationCodeValue === 0x10 && payload[3] === OPERATION_TYPE.CHECK ) { setTimeout(() => { if (this.feedStartRejector !== null) { reject(new Error(`[ACQ_TIMEOUT] code : ${operationCodeValue}`)); this.feedStartRejector = null; this.continueCommand(); } }, TIMEOUT); this.feedStartRejector = reject; } this.rejectTimer = new Timer( () => { logWebHook('rejectTImer 콜백함수 호출됨'); logRelease('rejectTImer 콜백함수 호출됨'); // TIMEOUT이 지났는데 resolver가 null이 아니라는 것은 onReadData를 통해 payload가 resolve되지 않았다는 뜻이기 때문에 reject하고 // 이미 reject한 명령에 대해 resolver가 돌면 안되니 resolver를 null 처리한다. if (this.resolver[operationCodeValue] !== null) { reject(new Error(`[TIMEOUT] code : ${operationCodeValue}`)); logWebHook(`[TIMEOUT] code fuck : ${operationCodeValue}`); logRelease(`[TIMEOUT] code fuck : ${operationCodeValue}`); this.resolver[operationCodeValue] = null; this.continueCommand(); } }, operationCodeValue === 0x10 ? 2000 : TIMEOUT, ); this.resolver[operationCodeValue] = resolve; this.sendMessage(payload); }; // 업데이트 중이면 UPDATE 이외의 명령은 무시 if (this.isUpdating && operationCodeValue !== 0x50) { reject(new Error(`[UPDATING] code : ${operationCodeValue}`)); return; } if ( this.feedStartRejector !== null || Object.values(this.resolver).some((item) => item !== null) ) { this.commandQueue.enqueue({ reject, execute: sendAsyncMessage }); } else { sendAsyncMessage(); } }); }
public async dispensing({ dispensingAmountInfo, onUnexpectedError, onFinish, }: { dispensingAmountInfo: DispensingAmountInformation[]; onUnexpectedError?: () => void; onFinish?: () => void; }): Promise<void> { const handleDispensing = async (isForCheck?: boolean): Promise<void> => { try { const dispensingResult = await this.setDispensingAmountAndStart( dispensingAmountInfo, isForCheck, ); logWebHook(`dispensingResult : ${dispensingResult}`); logRelease(`dispensingResult : ${dispensingResult}`); // 체크섬이 잘못됐을 때 재전송 if (dispensingResult[4] === 0x2) { throw new Error('checksumError'); } // 검증용 전송이 아닌 경우에는, 반환받은 결과 값을 통해 데이터 무결성을 검사한다. if (!!isForCheck === false) { const isDataEqual = this.verifyDispensingDataIntegrity( dispensingResult, dispensingAmountInfo, ); if (isDataEqual === false) { throw new Error('dataIntegrityError'); } } } catch (error) { if (error instanceof Error) { switch (error.message) { case 'checksumError': case 'dataIntegrityError': // 데이터가 변조된 경우, 동일한 명령어를 한번 더 내려보낸다. await handleDispensing(); return; } logWebHook('ㅎㅇ'); logRelease('ㅎㅇ'); onUnexpectedError?.(); } } }; // 토출명령을 내려보낸다. await handleDispensing(); // 토출명령 데이터 검증을 위해 commandType을 0x03(Check)으로 바꿔서 한번 더 보낸다. await handleDispensing(true); // 토출 완료 후의 동작 수행 onFinish?.(); }