/* * 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: } } }