335 lines
8.1 KiB
Go
335 lines
8.1 KiB
Go
/*
|
|
* Licensed to the Apache Software Foundation (ASF) under one
|
|
* or more contributor license agreements. See the NOTICE file
|
|
* distributed with this work for additional information
|
|
* regarding copyright ownership. The ASF licenses this file
|
|
* to you under the Apache License, Version 2.0 (the
|
|
* "License"); you may not use this file except in compliance
|
|
* with the License. You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing,
|
|
* software distributed under the License is distributed on an
|
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
* KIND, either express or implied. See the License for the
|
|
* specific language governing permissions and limitations
|
|
* under the License.
|
|
*/
|
|
|
|
package notification
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/apache/answer/internal/schema"
|
|
"github.com/apache/answer/pkg/token"
|
|
"github.com/segmentfault/pacman/log"
|
|
)
|
|
|
|
const defaultNewQuestionEmailWorkerQueueSize = 1024
|
|
|
|
const maxNewQuestionEmailWorkerQueueSize = 65536
|
|
|
|
const newQuestionEmailWorkerQueueSizeEnv = "NEW_QUESTION_NOTIFICATION_EMAIL_QUEUE_SIZE"
|
|
|
|
type newQuestionEmailTask struct {
|
|
UserIDs []string
|
|
QuestionTitle string
|
|
QuestionID string
|
|
Tags []string
|
|
TagIDs []string
|
|
}
|
|
|
|
type newQuestionEmailIntervalProvider func() time.Duration
|
|
|
|
type newQuestionEmailTimer interface {
|
|
C() <-chan time.Time
|
|
Stop()
|
|
}
|
|
|
|
type newQuestionEmailTimerFactory func(time.Duration) newQuestionEmailTimer
|
|
|
|
type newQuestionEmailWorker struct {
|
|
tasks chan newQuestionEmailTask
|
|
send newQuestionNotificationEmailSender
|
|
interval newQuestionEmailIntervalProvider
|
|
timerFactory newQuestionEmailTimerFactory
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
mu sync.RWMutex
|
|
closed bool
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
func newQuestionEmailWorkerWithDefaults(
|
|
interval newQuestionEmailIntervalProvider,
|
|
send newQuestionNotificationEmailSender,
|
|
) *newQuestionEmailWorker {
|
|
return newQuestionEmailWorkerWithBuffer(
|
|
interval,
|
|
send,
|
|
newRealNewQuestionEmailTimer,
|
|
newQuestionEmailWorkerQueueSize(),
|
|
)
|
|
}
|
|
|
|
func newQuestionEmailWorkerQueueSize() int {
|
|
return parseNewQuestionEmailWorkerQueueSize(os.Getenv(newQuestionEmailWorkerQueueSizeEnv))
|
|
}
|
|
|
|
func parseNewQuestionEmailWorkerQueueSize(value string) int {
|
|
value = strings.TrimSpace(value)
|
|
if len(value) == 0 {
|
|
return defaultNewQuestionEmailWorkerQueueSize
|
|
}
|
|
queueSize, err := strconv.ParseInt(value, 10, 64)
|
|
if err != nil || queueSize <= 0 {
|
|
return defaultNewQuestionEmailWorkerQueueSize
|
|
}
|
|
if queueSize > int64(maxNewQuestionEmailWorkerQueueSize) {
|
|
return maxNewQuestionEmailWorkerQueueSize
|
|
}
|
|
return int(queueSize)
|
|
}
|
|
|
|
func newQuestionEmailWorkerWithBuffer(
|
|
interval newQuestionEmailIntervalProvider,
|
|
send newQuestionNotificationEmailSender,
|
|
timerFactory newQuestionEmailTimerFactory,
|
|
bufferSize int,
|
|
) *newQuestionEmailWorker {
|
|
if interval == nil {
|
|
interval = newQuestionNotificationEmailSendInterval
|
|
}
|
|
if timerFactory == nil {
|
|
timerFactory = newRealNewQuestionEmailTimer
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
w := &newQuestionEmailWorker{
|
|
tasks: make(chan newQuestionEmailTask, bufferSize),
|
|
send: send,
|
|
interval: interval,
|
|
timerFactory: timerFactory,
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
}
|
|
w.wg.Add(1)
|
|
go w.run()
|
|
return w
|
|
}
|
|
|
|
func (w *newQuestionEmailWorker) TryEnqueue(task newQuestionEmailTask) bool {
|
|
if w == nil {
|
|
log.Warnf("[new_question_email] worker is nil, dropping new question email task")
|
|
return false
|
|
}
|
|
|
|
task = copyNewQuestionEmailTask(task)
|
|
|
|
w.mu.RLock()
|
|
defer w.mu.RUnlock()
|
|
|
|
if w.closed {
|
|
log.Warnf("[new_question_email] worker is closed, dropping new question email task for question %s", task.QuestionID)
|
|
return false
|
|
}
|
|
|
|
if w.ctx == nil {
|
|
log.Warnf("[new_question_email] worker context is nil, dropping new question email task for question %s", task.QuestionID)
|
|
return false
|
|
}
|
|
|
|
select {
|
|
case <-w.ctx.Done():
|
|
log.Warnf("[new_question_email] worker is canceled, dropping new question email task for question %s", task.QuestionID)
|
|
return false
|
|
default:
|
|
}
|
|
|
|
select {
|
|
case w.tasks <- task:
|
|
log.Debugf("[new_question_email] enqueued task for question %s to %d users", task.QuestionID, len(task.UserIDs))
|
|
return true
|
|
case <-w.ctx.Done():
|
|
log.Warnf("[new_question_email] worker canceled while enqueueing task for question %s", task.QuestionID)
|
|
return false
|
|
default:
|
|
log.Warnf("[new_question_email] queue is full, dropping new question email task for question %s", task.QuestionID)
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (w *newQuestionEmailWorker) Close() {
|
|
if w == nil {
|
|
return
|
|
}
|
|
|
|
w.mu.Lock()
|
|
if w.closed {
|
|
w.mu.Unlock()
|
|
return
|
|
}
|
|
w.closed = true
|
|
if w.cancel != nil {
|
|
w.cancel()
|
|
}
|
|
w.mu.Unlock()
|
|
|
|
w.wg.Wait()
|
|
if dropped := w.dropPendingTasks(); dropped > 0 {
|
|
log.Warnf("[new_question_email] dropped %d pending tasks during shutdown", dropped)
|
|
}
|
|
log.Infof("[new_question_email] worker closed")
|
|
}
|
|
|
|
func (w *newQuestionEmailWorker) run() {
|
|
defer w.wg.Done()
|
|
|
|
emailAttemptSent := false
|
|
for {
|
|
if w.ctx.Err() != nil {
|
|
return
|
|
}
|
|
|
|
select {
|
|
case <-w.ctx.Done():
|
|
return
|
|
case task := <-w.tasks:
|
|
if w.ctx.Err() != nil {
|
|
return
|
|
}
|
|
if !w.processTask(task, &emailAttemptSent) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (w *newQuestionEmailWorker) processTask(task newQuestionEmailTask, emailAttemptSent *bool) bool {
|
|
for _, userID := range task.UserIDs {
|
|
if w.ctx.Err() != nil {
|
|
return false
|
|
}
|
|
if *emailAttemptSent {
|
|
interval := w.interval()
|
|
if interval > 0 && !waitNewQuestionEmailInterval(w.ctx, interval, w.timerFactory) {
|
|
return false
|
|
}
|
|
}
|
|
if w.ctx.Err() != nil {
|
|
return false
|
|
}
|
|
if w.send == nil {
|
|
log.Errorf("[new_question_email] sender is nil, dropping email attempt for user %s question %s", userID, task.QuestionID)
|
|
*emailAttemptSent = true
|
|
continue
|
|
}
|
|
w.send(w.ctx, userID, task.newRawData())
|
|
*emailAttemptSent = true
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (w *newQuestionEmailWorker) dropPendingTasks() int {
|
|
dropped := 0
|
|
for {
|
|
select {
|
|
case <-w.tasks:
|
|
dropped++
|
|
default:
|
|
return dropped
|
|
}
|
|
}
|
|
}
|
|
|
|
func waitNewQuestionEmailInterval(
|
|
ctx context.Context,
|
|
interval time.Duration,
|
|
timerFactory newQuestionEmailTimerFactory,
|
|
) bool {
|
|
if interval <= 0 {
|
|
return true
|
|
}
|
|
if timerFactory == nil {
|
|
timerFactory = newRealNewQuestionEmailTimer
|
|
}
|
|
timer := timerFactory(interval)
|
|
defer timer.Stop()
|
|
|
|
select {
|
|
case <-timer.C():
|
|
return true
|
|
case <-ctx.Done():
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (task newQuestionEmailTask) newRawData() *schema.NewQuestionTemplateRawData {
|
|
return &schema.NewQuestionTemplateRawData{
|
|
QuestionTitle: task.QuestionTitle,
|
|
QuestionID: task.QuestionID,
|
|
UnsubscribeCode: token.GenerateToken(),
|
|
Tags: copyStringSlice(task.Tags),
|
|
TagIDs: copyStringSlice(task.TagIDs),
|
|
}
|
|
}
|
|
|
|
func newQuestionEmailTaskFromRawData(
|
|
userIDs []string,
|
|
rawData *schema.NewQuestionTemplateRawData,
|
|
) newQuestionEmailTask {
|
|
if rawData == nil {
|
|
return newQuestionEmailTask{UserIDs: copyStringSlice(userIDs)}
|
|
}
|
|
return newQuestionEmailTask{
|
|
UserIDs: copyStringSlice(userIDs),
|
|
QuestionTitle: rawData.QuestionTitle,
|
|
QuestionID: rawData.QuestionID,
|
|
Tags: copyStringSlice(rawData.Tags),
|
|
TagIDs: copyStringSlice(rawData.TagIDs),
|
|
}
|
|
}
|
|
|
|
func copyNewQuestionEmailTask(task newQuestionEmailTask) newQuestionEmailTask {
|
|
task.UserIDs = copyStringSlice(task.UserIDs)
|
|
task.Tags = copyStringSlice(task.Tags)
|
|
task.TagIDs = copyStringSlice(task.TagIDs)
|
|
return task
|
|
}
|
|
|
|
func copyStringSlice(values []string) []string {
|
|
if values == nil {
|
|
return nil
|
|
}
|
|
copied := make([]string, len(values))
|
|
copy(copied, values)
|
|
return copied
|
|
}
|
|
|
|
type realNewQuestionEmailTimer struct {
|
|
timer *time.Timer
|
|
}
|
|
|
|
func newRealNewQuestionEmailTimer(interval time.Duration) newQuestionEmailTimer {
|
|
return &realNewQuestionEmailTimer{timer: time.NewTimer(interval)}
|
|
}
|
|
|
|
func (t *realNewQuestionEmailTimer) C() <-chan time.Time {
|
|
return t.timer.C
|
|
}
|
|
|
|
func (t *realNewQuestionEmailTimer) Stop() {
|
|
if !t.timer.Stop() {
|
|
select {
|
|
case <-t.timer.C:
|
|
default:
|
|
}
|
|
}
|
|
}
|