Skip to content
isdnetworks
Go back

다시 보냈더니 앞부분이 두 번 갔다

공지를 회원에게 SMTP 로 보내는 작업이 있는데 한 번에 다 못 보내서 500명씩 나눠 보낸다. 중간에 실패해서 다시 돌렸더니 앞의 1,500명이 두 번 받았다.

Table of contents

Open Table of contents

원인 — 단위가 어긋나 있었다

발송 코드는 이랬다.

$members = $this->member_model->get_targets($noticeNo);
$chunks  = array_chunk($members, 500);

foreach ($chunks as $c) {
    $this->mailer->send_bulk($c, $subject, $body);
}

$this->db->update('notice', ['sent_yn' => 'Y'], ['notice_no' => $noticeNo]);

array_chunk 로 나눠 보내고 전부 끝난 뒤에 sent_yn 을 한 번 표시한다. 중간에 실패하면 표시가 안 되고 다시 돌리면 처음부터 다시 보낸다. 이미 보낸 조각인지 아닌지를 아는 자리가 없었다.

전체가 하나의 단위인데 실제 전송은 여러 조각으로 나간다. 표시는 전체 단위이고 전송은 조각 단위였다. 이 어긋남이 중복의 원인이었다.

조각마다 결과를 남겼다

발송 대상을 행으로 만들었다.

CREATE TABLE notice_send (
  send_no   INT NOT NULL AUTO_INCREMENT,
  notice_no INT NOT NULL,
  member_no INT NOT NULL,
  status    VARCHAR(10) NOT NULL DEFAULT 'ready',
  send_date DATETIME NULL,
  PRIMARY KEY (send_no),
  UNIQUE KEY uk_notice_member (notice_no, member_no),
  KEY idx_status (notice_no, status)
);

발송 전에 대상을 행으로 만들고 보낸 것만 상태를 바꾼다.

foreach ($chunks as $c) {
    $ok = $this->mailer->send_bulk($c, $subject, $body);
    if ($ok) {
        $nos = array_column($c, 'member_no');
        $this->db->where('notice_no', $noticeNo)
                 ->where_in('member_no', $nos)
                 ->update('notice_send', ['status' => 'sent', 'send_date' => date('Y-m-d H:i:s')]);
    }
}

다시 돌릴 때는 statusready 인 것만 대상이라 앞의 조각은 다시 나가지 않는다.

uk_notice_member 를 걸어 둔 것이 두 번째 방어선이었다. 대상을 만드는 코드를 두 번 돌려도 같은 회원이 두 행이 되지 않는다.

$this->db->query(
    "INSERT IGNORE INTO notice_send (notice_no, member_no, status)
     SELECT ?, member_no, 'ready' FROM member WHERE use_yn='Y'",
    [$noticeNo]
);

INSERT IGNORE 라 이미 있으면 무시된다. 몇 번을 돌려도 결과가 같다.

조각 안에서 실패하면

send_bulk 가 500명을 한 번에 보내는데 그중 일부만 실패할 수 있다. 전체 성공 여부만 받으면 어느 사람이 실패했는지 모른다.

$results = $this->mailer->send_bulk($c, $subject, $body);
// ['12': true, '13': false, ...]

foreach ($results as $memberNo => $ok) {
    $this->db->where('notice_no', $noticeNo)
             ->where('member_no', $memberNo)
             ->update('notice_send', [
                 'status'    => $ok ? 'sent' : 'failed',
                 'send_date' => date('Y-m-d H:i:s'),
             ]);
}

member_no 별로 결과를 받아 sentfailed 를 따로 적으니 실패한 사람만 다시 대상이 된다.

보냈는지 모르는 구간

한 가지 남은 것이 있다. send_bulk 가 도는 중에 죽으면 그 조각의 결과를 알 수 없다. 상태는 ready 인데 실제로는 나갔을 수 있다.

이 구간은 어느 쪽으로 정해도 완벽하지 않았다. 두 번 보내는 것과 안 보내는 것 중 무엇이 나은지를 물어봤고 공지는 두 번 받는 편이 낫다고 해서 다시 보내는 쪽으로 정했다. 결제 같은 것이었으면 반대로 정했을 것이다.

결과 — 진행 상황이 보이게 됐다

행으로 남기니 지금 어디까지 갔는지 조회할 수 있게 됐다.

SELECT status, COUNT(*) FROM notice_send WHERE notice_no = 12 GROUP BY status;
ready   1240
sent    3760
failed    12

이전에는 작업이 끝날 때까지 아무것도 몰랐다. 발송이 오래 걸릴 때 물어보는 사람에게 숫자로 답할 수 있게 됐고 failed 만 따로 뽑는 것도 같은 기록으로 됐다.

정말 두 번 안 가는지 확인하려고 일부러 두 번 돌려 봤다.

1회차: sent 3760
2회차: sent 3760 (변화 없음)

두 번째 실행에서 대상이 0건이면 정상이다. 대상이 0건인 것과 조회가 안 된 것을 구분하려고 개수도 찍었다.

log_message('info', "발송 대상 {$total}건 (notice {$noticeNo})");

정리


Share this post on:

Previous Post
두 프로젝트가 같은 것을 만들고 있었다
Next Post
어디까지 자동으로 할지 같이 정했다