Files
wehub-resource-sync 39dbe3a57d
CI / Check and lint (push) Failing after 1s
Lint / Lint (ubuntu-latest) (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 12:22:32 +08:00

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