Doug Lauder
2023-03-22 17:22:27 -04:00
коммит произвёл GitHub
родитель b61c096497
Коммит c943ed6859
13276 изменённых файлов: 1695615 добавлений и 223189 удалений

138
server/boards/utils/callbackqueue.go Обычный файл
Просмотреть файл

@@ -0,0 +1,138 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"context"
"runtime/debug"
"sync/atomic"
"time"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// CallbackFunc is a func that can enqueued in the callback queue and will be
// called when dequeued.
type CallbackFunc func() error
// CallbackQueue provides a simple thread pool for processing callbacks. Callbacks will
// be executed in the order in which they are enqueued, but no guarantees are provided
// regarding the order in which they finish (unless poolSize == 1).
type CallbackQueue struct {
name string
poolSize int
queue chan CallbackFunc
done chan struct{}
alive chan int
idone uint32
logger mlog.LoggerIFace
}
// NewCallbackQueue creates a new CallbackQueue and starts a thread pool to service it.
func NewCallbackQueue(name string, queueSize int, poolSize int, logger mlog.LoggerIFace) *CallbackQueue {
cn := &CallbackQueue{
name: name,
poolSize: poolSize,
queue: make(chan CallbackFunc, queueSize),
done: make(chan struct{}),
alive: make(chan int, poolSize),
logger: logger,
}
for i := 0; i < poolSize; i++ {
go cn.loop(i)
}
return cn
}
// Shutdown stops accepting enqueues and exits all pool threads. This method waits
// as long as the context allows for the threads to exit.
// Returns true if the pool exited, false on timeout.
func (cn *CallbackQueue) Shutdown(context context.Context) bool {
if !atomic.CompareAndSwapUint32(&cn.idone, 0, 1) {
// already shutdown
return true
}
// signal threads to exit
close(cn.done)
// wait for the threads to exit or timeout
count := 0
for count < cn.poolSize {
select {
case <-cn.alive:
count++
case <-context.Done():
return false
}
}
// try to drain any remaining callbacks
for {
select {
case f := <-cn.queue:
cn.exec(f)
case <-context.Done():
return false
default:
return true
}
}
}
// Enqueue adds a callback to the queue.
func (cn *CallbackQueue) Enqueue(f CallbackFunc) {
if atomic.LoadUint32(&cn.idone) != 0 {
cn.logger.Debug("CallbackQueue skipping enqueue, notifier is shutdown", mlog.String("name", cn.name))
return
}
select {
case cn.queue <- f:
default:
start := time.Now()
cn.queue <- f
dur := time.Since(start)
cn.logger.Warn("CallbackQueue queue backlog", mlog.String("name", cn.name), mlog.Duration("wait_time", dur))
}
}
func (cn *CallbackQueue) loop(id int) {
defer func() {
cn.logger.Trace("CallbackQueue thread exited", mlog.String("name", cn.name), mlog.Int("id", id))
cn.alive <- id
}()
for {
select {
case f := <-cn.queue:
cn.exec(f)
case <-cn.done:
return
}
}
}
func (cn *CallbackQueue) exec(f CallbackFunc) {
// don't let a panic in the callback exit the thread.
defer func() {
if r := recover(); r != nil {
stack := debug.Stack()
cn.logger.Error("CallbackQueue callback panic",
mlog.String("name", cn.name),
mlog.Any("panic", r),
mlog.String("stack", string(stack)),
)
}
}()
if err := f(); err != nil {
cn.logger.Error("CallbackQueue callback error", mlog.String("name", cn.name), mlog.Err(err))
}
}

67
server/boards/utils/callbackqueue_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,67 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"context"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func Test_newChangeNotifier(t *testing.T) {
logger := mlog.CreateConsoleTestLogger(false, mlog.LvlDebug)
t.Run("startup, shutdown", func(t *testing.T) {
cn := NewCallbackQueue("test1", 100, 5, logger)
var callbackCount int32
callback := func() error {
atomic.AddInt32(&callbackCount, 1)
return nil
}
const loops = 500
for i := 0; i < loops; i++ {
cn.Enqueue(callback)
// don't peg the cpu
if i%20 == 0 {
time.Sleep(time.Millisecond * 1)
}
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
ok := cn.Shutdown(ctx)
assert.True(t, ok, "shutdown should return true (no timeout)")
assert.Equal(t, int32(loops), atomic.LoadInt32(&callbackCount))
})
t.Run("handle panic", func(t *testing.T) {
cn := NewCallbackQueue("test2", 100, 5, logger)
var callbackCount int32
callback := func() error {
atomic.AddInt32(&callbackCount, 1)
panic("oh no!")
}
const loops = 5
for i := 0; i < loops; i++ {
cn.Enqueue(callback)
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
ok := cn.Shutdown(ctx)
assert.True(t, ok, "shutdown should return true (no timeout)")
assert.Equal(t, int32(loops), atomic.LoadInt32(&callbackCount))
})
}

23
server/boards/utils/debug.go Обычный файл
Просмотреть файл

@@ -0,0 +1,23 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"os"
"strings"
)
// IsRunningUnitTests returns true if this instance of FocalBoard is running unit or integration tests.
func IsRunningUnitTests() bool {
testing := os.Getenv("FOCALBOARD_UNIT_TESTING")
if testing == "" {
return false
}
switch strings.ToLower(testing) {
case "1", "t", "y", "true", "yes":
return true
}
return false
}

15
server/boards/utils/links.go Обычный файл
Просмотреть файл

@@ -0,0 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import "fmt"
// MakeCardLink creates fully qualified card links based on card id and parents.
func MakeCardLink(serverRoot string, teamID string, boardID string, cardID string) string {
return fmt.Sprintf("%s/team/%s/%s/0/%s", serverRoot, teamID, boardID, cardID)
}
func MakeBoardLink(serverRoot string, teamID string, board string) string {
return fmt.Sprintf("%s/team/%s/%s", serverRoot, teamID, board)
}

8
server/boards/utils/testUtils.go Обычный файл
Просмотреть файл

@@ -0,0 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import "github.com/stretchr/testify/mock"
var Anything = mock.MatchedBy(func(interface{}) bool { return true })

130
server/boards/utils/utils.go Обычный файл
Просмотреть файл

@@ -0,0 +1,130 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"encoding/json"
"path"
"reflect"
"time"
mm_model "github.com/mattermost/mattermost-server/v6/model"
)
type IDType byte
const (
IDTypeNone IDType = '7'
IDTypeTeam IDType = 't'
IDTypeBoard IDType = 'b'
IDTypeCard IDType = 'c'
IDTypeView IDType = 'v'
IDTypeSession IDType = 's'
IDTypeUser IDType = 'u'
IDTypeToken IDType = 'k'
IDTypeBlock IDType = 'a'
IDTypeAttachment IDType = 'i'
)
// NewId is a globally unique identifier. It is a [A-Z0-9] string 27
// characters long. It is a UUID version 4 Guid that is zbased32 encoded
// with the padding stripped off, and a one character alpha prefix indicating the
// type of entity or a `7` if unknown type.
func NewID(idType IDType) string {
return string(idType) + mm_model.NewId()
}
// GetMillis is a convenience method to get milliseconds since epoch.
func GetMillis() int64 {
return mm_model.GetMillis()
}
// GetMillisForTime is a convenience method to get milliseconds since epoch for provided Time.
func GetMillisForTime(thisTime time.Time) int64 {
return mm_model.GetMillisForTime(thisTime)
}
// GetTimeForMillis is a convenience method to get time.Time for milliseconds since epoch.
func GetTimeForMillis(millis int64) time.Time {
return mm_model.GetTimeForMillis(millis)
}
// SecondsToMillis is a convenience method to convert seconds to milliseconds.
func SecondsToMillis(seconds int64) int64 {
return seconds * 1000
}
func StructToMap(v interface{}) (m map[string]interface{}) {
b, _ := json.Marshal(v)
_ = json.Unmarshal(b, &m)
return
}
func intersection(a []interface{}, b []interface{}) []interface{} {
set := make([]interface{}, 0)
hash := make(map[interface{}]bool)
av := reflect.ValueOf(a)
bv := reflect.ValueOf(b)
for i := 0; i < av.Len(); i++ {
el := av.Index(i).Interface()
hash[el] = true
}
for i := 0; i < bv.Len(); i++ {
el := bv.Index(i).Interface()
if _, found := hash[el]; found {
set = append(set, el)
}
}
return set
}
func Intersection(x ...[]interface{}) []interface{} {
if len(x) == 0 {
return nil
}
if len(x) == 1 {
return x[0]
}
result := x[0]
i := 1
for i < len(x) {
result = intersection(result, x[i])
i++
}
return result
}
func IsCloudLicense(license *mm_model.License) bool {
return license != nil &&
license.Features != nil &&
license.Features.Cloud != nil &&
*license.Features.Cloud
}
func DedupeStringArr(arr []string) []string {
hashMap := map[string]bool{}
for _, item := range arr {
hashMap[item] = true
}
dedupedArr := make([]string, len(hashMap))
i := 0
for key := range hashMap {
dedupedArr[i] = key
i++
}
return dedupedArr
}
func GetBaseFilePath() string {
return path.Join("boards", time.Now().Format("20060102"))
}