`/presence/update`에 대한 무한한 요청

음, 엄밀히 말해 bug은 아닐 수도 있고, 원래 그렇게 설계된 것일 수도 있지만, JS 쪽에서 presence가 어떻게 업데이트되는지에 대해 몇 가지 의문이 있습니다. 관련 파일은 app/assets/javascripts/discourse/app/services/presence.js입니다.

지금까지 파악한 바로는 JS 클라이언트는 다음과 같은 동작을 합니다:

  • 업데이트 대기열이 비어 있지 않을 경우, 업데이트를 1초에 1회로 스로틀링합니다.
  • 변경 사항이 없을 경우, 30초에 1회 요청을 보냅니다.

관찰해 본 결과, 서버에 어떤 이유로든 오류가 발생하면 JS가 1초마다 계속 요청을 보내는 것을 확인했습니다. 또한 콘솔에 Uncaught (in promise) 오류가 565번째 줄에서 발생합니다.
서버에 과부하를 주지 않도록 N번의 오류 후에는 요청을 중단하는 것이 아닌가 싶습니다.

재현 방법

presence_controller.rb의 update 메서드에서 오류를 시뮬레이션하세요. (제 경우 데모를 위해 status 405를 렌더링했습니다)

  def update

    # JS client is designed to throttle to one request per second
    # When no changes are being made, it makes one request every 30 seconds
    RateLimiter.new(nil, "update-presence-#{current_user.id}", 20, 10.seconds).performed!
    render json: { error: "Not authorized" }, status: 405
    return
    ...

그 다음 클라이언트에서 presence 관리가 있는 화면으로 이동하세요. 저는 토픽 답변 화면을 선택하고 답변을 작성하기 시작했습니다.

결과는 영원히 계속되며, 1초마다 요청이 반복됩니다.

1개의 좋아요

또한 30초마다 presence를 다시 계산하는 것이 정말로 필요한가요? 다음과 같은 계산이 이미 이루어지고 있기 때문입니다:

  • enter 액션이 수행될 때 계산
  • leave 액션이 수행될 때 계산
  • userPresence가 변경될 때 계산

기능 요청이 오류 발생 시 백오프(back off)를 적용하자는 건가요? 그러면 반복 오류가 발생할 경우 최대 30초 정도까지 백오프가 적용될 텐데요…

1개의 좋아요

@AhmedLoud 이 문제는 FIX: use `_presentChannels.size` instead of `_presentChannels.length`… · discourse/discourse@589add7 · GitHub 에서 수정한 것과 같은 문제인가요, 아니면 별개의 문제인가요?

1개의 좋아요

@david 오해해서 죄송합니다.

제 첫 번째 주제 게시글은 이 내용과 관련이 없습니다: FIX: use `_presentChannels.size` instead of `_presentChannels.length`… · discourse/discourse@589add7 · GitHub

백엔드에서 오류가 발생하면, 30초마다 예약된 업데이트가 없더라도 이벤트 큐가 비어 있지 않기 때문에 알고리즘이 1초마다 다시 시도합니다:

_scheduleNextUpdate 내에서

   } else if (this._queuedEvents.length > 0) {
      this._cancelTimer();
      cancel(this._debounceTimer);
      this._debounceTimer = debounce(
        this,
        this._throttledUpdateServer,
        this._presenceDebounceMs // 이건 1초인 것 같습니다
      );

사실 _updateServer 메서드 → 오류 발생 시 이벤트가 큐에 다시 들어갑니다:

  } catch (e) {
      // 실패한 이벤트를 다음을 위해 큐에 다시 넣습니다
      this._queuedEvents.unshift(...queue);

이것은 나중에 발견한 내용입니다… 모든 것이 정상적으로 작동할 때(오류가 없는 경우), 정말로 30초마다 서버를 계속 업데이트해야 하나요?

} else if (
      !this._nextUpdateTimer &&
      this._presentChannels.size > 0 &&
      !isTesting()
    ) {
      this._nextUpdateTimer = discourseLater(
        this,
        this._throttledUpdateServer,
        PRESENCE_INTERVAL_S * 1000 // 이건 30초입니다
      );
    }
1개의 좋아요

백오프 기능이 없는 것 같습니다. 어떤 오류가 발생하든 멈추지 않고 1초마다 계속 재시도합니다. 하지만 제가 무언가 놓쳤을 수도 있습니다.

1개의 좋아요

음, 30초마다 다시 "체크인"해야 하는 이유를 이제 이해한 것 같습니다.

PresenceChannelAutoLeave라는 백그라운드 잡이 매 분마다 실행되며, 만료된 멤버들이 채널을 나가도록 합니다.

presence_channel.rb의 present(user:, client_id:) 메서드에도 다음과 같이 주석으로 명시되어 있습니다:

    # Mark a user's client as present in this channel. The client_id should be unique per
    # browser tab. This method should be called repeatedly (at least once every DEFAULT_TIMEOUT)
    # while the user is present in the channel.
    def present(user:, client_id:)

여전히 매 초마다 발생하는 오류 부분에 대해서는 궁금합니다.

429 ‘rate limited’ 오류는 이미 올바르게 처리하고 있었습니다. 하지만 기타 예상치 못한 오류에 대해서는 지수 백오프 로직(최대 30초)을 추가했습니다. 이 문제를 알려주셔서 감사합니다, @AhmedLoud

3개의 좋아요

이 주제는 9일 후 자동으로 닫혔습니다. 새로운 답글은 더 이상 허용되지 않습니다.