Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add pool of resources to maintain locks for #370

Merged
merged 7 commits into from
Jan 18, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
add pool of resources to maintain locks for
  • Loading branch information
fsuhrau committed Dec 28, 2022
commit dc35d902591ab406b5fc5aeade7f11b7ac508a58
5 changes: 5 additions & 0 deletions bot/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ type Conditional interface {
IsEnabled() bool
}

// Runnable indicates that the command executes a go function
type Runnable interface {
RunAsync()
}

// HelpProvider can be provided by a command to add information within "help" command
type HelpProvider interface {
// GetHelp each command should provide information, like a description or examples
Expand Down
1 change: 1 addition & 0 deletions bot/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package config
// Config contains the full config structure of this bot
type Config struct {
Slack Slack `mapstructure:"slack"`
Pool Pool `mapstructure:"pool"`
Jenkins Jenkins `mapstructure:"jenkins"`
Jira Jira `mapstructure:"jira"`
StoragePath string `mapstructure:"storage_path"`
Expand Down
23 changes: 23 additions & 0 deletions bot/config/pool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package config

import "time"

// Pool config contains the Resources of the Pool
type Pool struct {
LockDuration time.Duration
NotifyExpire time.Duration
Resources []*Resource
}

// Resource config contains definitions about the
type Resource struct {
Name string
ExplicitLock bool
Addresses []string
Features []string
}

// IsEnabled checks if there are resources in the pool
func (c *Pool) IsEnabled() bool {
return len(c.Resources) > 0
}
12 changes: 12 additions & 0 deletions bot/listener.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package bot

import (
"context"
"os"
"os/signal"
"syscall"
Expand All @@ -14,8 +15,19 @@ import (
"github.com/slack-go/slack/socketmode"
)

func (b *Bot) StartRunnables(ctx context.Context) {
for _, cmd := range b.commands.commands {
if runnable, ok := cmd.(Runnable); ok {
go runnable.RunAsync()
}
}
}

// Run is blocking method to handle new incoming events...from different sources
func (b *Bot) Run(ctx *util.ServerContext) {

b.StartRunnables(ctx)

// listen for old/deprecated RTM connection
// https://api.slack.com/rtm
var rtmChan chan slack.RTMEvent
Expand Down
27 changes: 27 additions & 0 deletions client/slack.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ type SlackClient interface {
// SendEphemeralMessage sends a message just visible to the current user
SendEphemeralMessage(ref msg.Ref, text string, options ...slack.MsgOption)

SendBlockMessageToUser(user string, blocks []slack.Block, options ...slack.MsgOption) string

// SendBlockMessage will send Slack Blocks/Sections to the target
SendBlockMessage(ref msg.Ref, blocks []slack.Block, options ...slack.MsgOption) string

Expand Down Expand Up @@ -234,6 +236,31 @@ func (s *Slack) SendToUser(user string, text string) {
s.SendMessage(message, text)
}

// SendBlockMessage will send Slack Blocks/Sections to the target
func (s *Slack) SendBlockMessageToUser(user string, blocks []slack.Block, options ...slack.MsgOption) string {
// check if a real username was passed -> we need the user-id here
userID, _ := GetUserIDAndName(user)
if userID == "" {
log.Errorf("Invalid user: %s", user)
return ""
}

conversationOptions := &slack.OpenConversationParameters{
Users: []string{userID},
}

channel, _, _, err := s.Client.OpenConversation(conversationOptions)
if err != nil {
log.WithError(err).Errorf("Cannot open channel")
return ""
}

message := msg.Message{}
message.Channel = channel.ID

return s.SendBlockMessage(message, blocks, options...)
}

// CanHandleInteractions checks if we have a slack connections which can inform us about events/interactions, like pressed buttons?
func (s *Slack) CanHandleInteractions() bool {
return s.config.CanHandleInteractions()
Expand Down
4 changes: 4 additions & 0 deletions command/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/innogames/slack-bot/v2/command/games"
"github.com/innogames/slack-bot/v2/command/jenkins"
"github.com/innogames/slack-bot/v2/command/jira"
"github.com/innogames/slack-bot/v2/command/pool"
"github.com/innogames/slack-bot/v2/command/pullrequest"
"github.com/innogames/slack-bot/v2/command/queue"
"github.com/innogames/slack-bot/v2/command/variables"
Expand Down Expand Up @@ -64,6 +65,9 @@ func GetCommands(slackClient client.SlackClient, cfg config.Config) *bot.Command
// aws
commands.Merge(aws.GetCommands(cfg.Aws, base))

// pool
commands.Merge(pool.GetCommands(&cfg.Pool, base))

return commands
}

Expand Down
30 changes: 30 additions & 0 deletions command/pool/commands.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package pool

import (
"github.com/innogames/slack-bot/v2/bot"
"github.com/innogames/slack-bot/v2/bot/config"
"github.com/innogames/slack-bot/v2/client"
)

// GetCommands will return a list of available Pool commands...if the config is set!
func GetCommands(cfg *config.Pool, slackClient client.SlackClient) bot.Commands {
var commands bot.Commands

if !cfg.IsEnabled() {
return commands
}

p := GetNewPool(cfg)

commands.AddCommand(
newPoolCommands(slackClient, cfg, p),
)

return commands
}

var category = bot.Category{
Name: "Pool",
Description: "Lock/Unlock/Manage Resources of a Pool",
HelpURL: "https://github.com/innogames/slack-bot",
brainexe marked this conversation as resolved.
Show resolved Hide resolved
}
231 changes: 231 additions & 0 deletions command/pool/pool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
package pool

import (
"fmt"
"sort"
"sync"
"time"

"github.com/innogames/slack-bot/v2/bot/config"
"github.com/innogames/slack-bot/v2/bot/storage"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)

const (
storageKey = "pool"
)

var (
ResourceLockedByDifferentUser = fmt.Errorf("resources locked by different user")
NoLockedResourceFound = fmt.Errorf("no locked resource found")
NoResourceAvailable = fmt.Errorf("no resource available")
)

// ResourceLock struct to hold and store the current locks
type ResourceLock struct {
Resource config.Resource `json:"-"`
User string
Reason string
WarningSend bool `json:"-"`
LockUntil time.Time
}

type pool struct {
locks map[*config.Resource]*ResourceLock
lockDuration time.Duration
mu sync.RWMutex
}

// GetNewPool create a new pool and initialize it by the local storage
func GetNewPool(cfg *config.Pool) *pool {
var p pool

p.lockDuration = cfg.LockDuration

p.locks = make(map[*config.Resource]*ResourceLock)
for _, resource := range cfg.Resources {
p.locks[resource] = nil
}

keys, _ := storage.GetKeys(storageKey)
if len(keys) == 0 {
return &p
}

var lock ResourceLock
for _, key := range keys {
if err := storage.Read(storageKey, key, &lock); err != nil {
log.Errorf("[Pool] unable to restore lock for '%s': %s", key, err)
continue
}

for k, _ := range p.locks {
if k.Name == key {
lock.Resource = *k
p.locks[k] = &lock
break
}
}
}
return &p
}

// Lock a resource in the pool for a user
func (p *pool) Lock(user, reason, resourceName string) (*ResourceLock, error) {
specificResource := len(resourceName) > 0

for k, v := range p.locks {
if v != nil {
// it's already in used
continue
}

if !specificResource && k.ExplicitLock {
// resource can be locked only specifically
continue
}

if specificResource && k.Name != resourceName {
// specific resource should be locked but it's not this one.
continue
}

resourceLock := &ResourceLock{
Resource: *k,
User: user,
Reason: reason,
LockUntil: time.Now().Add(p.lockDuration),
}

p.mu.Lock()
defer p.mu.Unlock()

p.locks[k] = resourceLock

if err := storage.Write(storageKey, k.Name, resourceLock); err != nil {
log.Error(errors.Wrap(err, "error while storing pool lock entry"))
}
return resourceLock, nil
}

return nil, NoResourceAvailable
}

// Extend the lock of a resource in the pool for a user
func (p *pool) ExtendLock(user, resourceName, duration string) (*ResourceLock, error) {
for k, v := range p.locks {
if v == nil {
continue
}

if k.Name != resourceName {
continue
}

if v.User != user {
return nil, ResourceLockedByDifferentUser
}

d, err := time.ParseDuration(duration)
if err != nil {
return nil, err
}

v.LockUntil = v.LockUntil.Add(d)
v.WarningSend = false

p.locks[k] = v

if err := storage.Delete(storageKey, k.Name); err != nil {
log.Error(errors.Wrap(err, "error while storing pool lock entry"))
}
if err := storage.Write(storageKey, k.Name, v); err != nil {
log.Error(errors.Wrap(err, "error while storing pool lock entry"))
}

return v, nil
}

return nil, NoLockedResourceFound
}

// Unlock a resource of a user
func (p *pool) Unlock(user, resourceName string) error {
for k, v := range p.locks {
if v == nil {
continue
}

if k.Name != resourceName {
continue
}

if v.User != user {
return ResourceLockedByDifferentUser
}

p.locks[k] = nil

if err := storage.Delete(storageKey, k.Name); err != nil {
log.Error(errors.Wrap(err, "error while storing pool lock entry"))
}
}

return nil
}

type lockSorter struct {
resources []*ResourceLock
}

func (s *lockSorter) Len() int {
return len(s.resources)
}
func (s *lockSorter) Swap(i, j int) {
s.resources[i], s.resources[j] = s.resources[j], s.resources[i]
}
func (s *lockSorter) Less(i, j int) bool {
return s.resources[i].Resource.Name < s.resources[j].Resource.Name
}

// Get a sorted list of all active locks of a user / all users if userName = ""
func (p *pool) GetLocks(userName string) []*ResourceLock {
var locked []*ResourceLock
byUser := len(userName) > 0
for _, v := range p.locks {
if v != nil && (!byUser || userName == v.User) {
locked = append(locked, v)
}
}
sorter := &lockSorter{resources: locked}
sort.Sort(sorter)
return sorter.resources
}

type resourceSorter struct {
resources []*config.Resource
}

func (s *resourceSorter) Len() int {
return len(s.resources)
}
func (s *resourceSorter) Swap(i, j int) {
s.resources[i], s.resources[j] = s.resources[j], s.resources[i]
}
func (s *resourceSorter) Less(i, j int) bool {
return s.resources[i].Name < s.resources[j].Name
}

// Get a sorted list of all free / unlocked resources
func (p *pool) GetFree() []*config.Resource {
var free []*config.Resource
for k, v := range p.locks {
if v == nil {
free = append(free, k)
}
}
sorter := &resourceSorter{resources: free}
sort.Sort(sorter)
return sorter.resources
}
Loading