Files
fission-src/pkg/throttler/throttler.go
T

228 lines
5.6 KiB
Go

/*
Copyright 2018 The Fission Authors.
Licensed 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 throttler
import (
"sync"
"time"
"github.com/pkg/errors"
)
type throttlerOperationType int
const (
GET throttlerOperationType = iota
DELETE
EXPIRE
)
type (
// actionLock is a lock that indicates whether a resource with
// certain key is being updated or not.
actionLock struct {
wg *sync.WaitGroup
ctimestamp time.Time // creation time of lock
timeExpiry time.Duration
}
// Throttler is a simple throttling mechanism that provides the abil-
// ity to limit the total amount of requests to do the same thing at
// the same time.
//
// In router, for example, multiple goroutines may try to get the la-
// test service URL from executor when there is no service URL entry
// in the cache and caused executor overloaded because of receiving
// massive requests. With throttler, we can easily limit there is at
// most one requests being sent to executor.
Throttler struct {
requestChan chan *request
locks map[string]*actionLock
lockTimeExpiry time.Duration
}
request struct {
requestType throttlerOperationType
responseChan chan *response
resourceKey string
}
response struct {
lock *actionLock
firstGoroutine bool // denote this goroutine is the first goroutine
}
)
func (l *actionLock) isOld() bool {
return time.Since(l.ctimestamp) > l.timeExpiry
}
func (l *actionLock) wait() error {
ch := make(chan struct{})
go func(wg *sync.WaitGroup, ch chan struct{}) {
wg.Wait()
close(ch)
}(l.wg, ch)
select {
case <-ch:
return nil
case <-time.After(l.timeExpiry):
return errors.New("Error waiting for actionLock to be released: Exceeded timeout")
}
}
// MakeThrottler returns a throttler that able to limit total amounts of goroutines from
// doing the same thing at the same time.
func MakeThrottler(timeExpiry time.Duration) *Throttler {
tr := &Throttler{
requestChan: make(chan *request),
locks: make(map[string]*actionLock),
lockTimeExpiry: timeExpiry,
}
go tr.service()
go tr.expiryService()
return tr
}
func (tr *Throttler) service() {
for {
req := <-tr.requestChan
switch req.requestType {
case GET:
lock, ok := tr.locks[req.resourceKey]
if ok && !lock.isOld() {
req.responseChan <- &response{
lock: lock, firstGoroutine: false,
}
continue
} else if ok && lock.isOld() {
// in case that one goroutine occupy the update lock for long time
lock.wg.Done()
}
lock = &actionLock{
wg: &sync.WaitGroup{},
ctimestamp: time.Now(),
timeExpiry: tr.lockTimeExpiry,
}
lock.wg.Add(1)
tr.locks[req.resourceKey] = lock
req.responseChan <- &response{
lock: lock, firstGoroutine: true,
}
case DELETE:
lock, ok := tr.locks[req.resourceKey]
if ok {
lock.wg.Done()
delete(tr.locks, req.resourceKey)
}
case EXPIRE:
for k, v := range tr.locks {
if v.isOld() {
delete(tr.locks, k)
v.wg.Done()
}
}
}
}
}
// RunOnce accepts two arguments:
// 1. function metadata:
// It's used to check whether the actionLock of a function exists.
//
// If not exists, an actionLock will be inserted into the map with
// passing key. Then, throttler pass true to callbackFunc to indi-
// cate this goroutine is the first goroutine and is responsible for
// calling backend service, like updating service URL entry in router.
//
// If exists, the goroutines have to wait until the first goroutine
// finishes the update process.
//
// 2. callback function:
// The callback function is a function accepts one bool argument which
// indicates whether a goroutine is responsible for getting/updating a
// resource from backend service or waiting for the first goroutine to
// be finished.
//
// Example callback function:
//
// func(firstToTheLock bool) (interface{}, error) {
// var u *url.URL
//
// if firstToTheLock { // first to the service url
// // Call to backend services then do something else.
// // For example, get service url from executor then update router cache.
// } else {
// // Do something here.
// // For example, get service url from cache
// }
//
// return AnythingYouWant{}, error
// }
func (tr *Throttler) RunOnce(resourceKey string,
callbackFunc func(bool) (interface{}, error)) (interface{}, error) {
ch := make(chan *response)
tr.requestChan <- &request{
requestType: GET,
responseChan: ch,
resourceKey: resourceKey,
}
resp := <-ch
// if we are not the first one, wait for the first goroutine to finish its task
if !resp.firstGoroutine {
err := resp.lock.wait()
if err != nil {
return nil, err
}
}
// release actionLock so that other goroutines can take over the responsibility if failed.
defer func() {
go func() {
tr.requestChan <- &request{
requestType: DELETE,
resourceKey: resourceKey,
}
}()
}()
return callbackFunc(resp.firstGoroutine)
}
// expiryService periodically expires time-out locks.
// Normally, we don't need to do this just in case any of goroutine didn't release lock.
func (tr *Throttler) expiryService() {
for {
time.Sleep(time.Minute)
tr.requestChan <- &request{
requestType: EXPIRE,
}
}
}