Skip to content

Commit

Permalink
Merge pull request #31 from serverscom/aggregate-servers
Browse files Browse the repository at this point in the history
add servers collector abstraction for aggregating servers order
  • Loading branch information
olegy89 authored Oct 2, 2024
2 parents 2b89f69 + 5016c1c commit 5cd899b
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 20 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ website/node_modules
*.test
*.iml
vendor
.vscode

website/vendor

Expand Down
3 changes: 3 additions & 0 deletions serverscom/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,8 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) {
client.SetupUserAgent("terraform-provider-serverscom")
cache = NewCache(client)

serverCollector = NewServerCollector(client)
serverCollector.Run()

return client, nil
}
58 changes: 38 additions & 20 deletions serverscom/resource_serverscom_dedicated_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func resourceServerscomDedicatedServer() *schema.Resource {
Delete: resourceServerscomDedicatedServerDelete,
Create: resourceServerscomDedicatedServerCreate,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
StateContext: schema.ImportStatePassthroughContext,
},

Timeouts: &schema.ResourceTimeout{
Expand Down Expand Up @@ -300,22 +300,23 @@ func resourceServerscomDedicatedServerDelete(d *schema.ResourceData, meta interf
}

func resourceServerscomDedicatedServerCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*scgo.Client)

var location *scgo.Location
var serverModel *scgo.ServerModelOption
var operatingSystem *scgo.OperatingSystemOption
var publicUplink *scgo.UplinkOption
var bandwidth *scgo.BandwidthOption
var privateUplink *scgo.UplinkOption
var slots []scgo.DedicatedServerSlotInput
var layouts []scgo.DedicatedServerLayoutInput
var dedicatedServers []scgo.DedicatedServer

var publicIpv4NetworkId *string
var privateIpv4NetworkId *string

var err error
var (
location *scgo.Location
serverModel *scgo.ServerModelOption
operatingSystem *scgo.OperatingSystemOption
publicUplink *scgo.UplinkOption
bandwidth *scgo.BandwidthOption
privateUplink *scgo.UplinkOption
slots []scgo.DedicatedServerSlotInput
layouts []scgo.DedicatedServerLayoutInput
dedicatedServers []scgo.DedicatedServer
dedicatedServer scgo.DedicatedServer

publicIpv4NetworkId *string
privateIpv4NetworkId *string

err error
)

input := scgo.DedicatedServerCreateInput{}

Expand All @@ -329,9 +330,10 @@ func resourceServerscomDedicatedServerCreate(d *schema.ResourceData, meta interf
privateIpv4NetworkId = &privateIpv4NetworkIdValue
}

hostname := d.Get("hostname").(string)
input.Hosts = []scgo.DedicatedServerHostInput{
{
Hostname: d.Get("hostname").(string),
Hostname: hostname,
PublicIPv4NetworkID: publicIpv4NetworkId,
PrivateIPv4NetworkID: privateIpv4NetworkId,
},
Expand Down Expand Up @@ -431,16 +433,32 @@ func resourceServerscomDedicatedServerCreate(d *schema.ResourceData, meta interf

ctx := context.TODO()

dedicatedServers, err = client.Hosts.CreateDedicatedServers(ctx, input)
resultChan, err := serverCollector.AddRequest(ctx, serverModel.Name, &input)
if err != nil {
return err
}

// waiting for result from collector
result := <-resultChan
if result.Error != nil {
return result.Error
}

dedicatedServers = result.Servers

if len(dedicatedServers) == 0 {
return fmt.Errorf("Invalid dedicated servers count returned by api")
}

dedicatedServer := dedicatedServers[0]
// find corresponding server by title matching hostname
for _, server := range dedicatedServers {
if server.Title == hostname {
dedicatedServer = server
}
}
if dedicatedServer.ID == "" {
return fmt.Errorf("Can't find the server with title '%s' in api response", hostname)
}

d.SetId(dedicatedServer.ID)

Expand Down
150 changes: 150 additions & 0 deletions serverscom/servers_collector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package serverscom

import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"sync"
"time"

scgo "github.com/serverscom/serverscom-go-client/pkg"
)

var (
serverCollector *ServerCollector

// when timer expires the collector triggers the ExecuteRequests method
serverCollectorTimer = 5 * time.Second
)

// ServerCollector represents server collector abstraction.
// It accept requests from create events for 'serverscom_dedicated_server' resources
// groups it by checksum based on all server fields except 'hosts' and create these servers in one batch request
type ServerCollector struct {
Client *scgo.Client
Requests map[string][]*Request
Mutex sync.Mutex
Timer *time.Timer
}

// Request represents a request with create server input and result channel
type Request struct {
Input *scgo.DedicatedServerCreateInput
ResultChan chan Result
}

// Result represents the api response and error.
// It's used for sending back the result to the create event through the ResultChan
type Result struct {
Servers []scgo.DedicatedServer
Error error
}

// NewServerCollector creates new ServerCollector
func NewServerCollector(client *scgo.Client) *ServerCollector {
return &ServerCollector{
Client: client,
Requests: make(map[string][]*Request),
Timer: time.NewTimer(5 * time.Second),
}
}

// AddRequest adds request to server collector
// Each request resets the serverCollectorTimer
func (sc *ServerCollector) AddRequest(ctx context.Context, model string, request *scgo.DedicatedServerCreateInput) (<-chan Result, error) {
sc.Mutex.Lock()
defer sc.Mutex.Unlock()

resultChan := make(chan Result, 1)
checksum, err := calculateServerChecksum(*request)
if err != nil {
return nil, err
}
sc.Requests[checksum] = append(sc.Requests[checksum], &Request{Input: request, ResultChan: resultChan})

if !sc.Timer.Stop() {
select {
case <-sc.Timer.C:
default:
}
}
sc.Timer.Reset(serverCollectorTimer)

return resultChan, nil
}

// ExecuteRequests triggers when timer expires and runs CreateServersBatch for each requests checksum group
func (sc *ServerCollector) ExecuteRequests() {
sc.Mutex.Lock()
defer sc.Mutex.Unlock()

for checksum, requests := range sc.Requests {
CreateServersBatch(sc.Client, requests)
sc.Requests[checksum] = nil
}
}

// Run runs the collector to listen for requests
func (sc *ServerCollector) Run() {
go func() {
for {
<-sc.Timer.C
sc.ExecuteRequests()
}
}()
}

// CreateServersBatch aggregates hostnames from all requests in one input and creates these servers in one api request
func CreateServersBatch(client *scgo.Client, requests []*Request) {
if len(requests) == 0 {
return
}
for _, req := range requests {
defer close(req.ResultChan)
}

// combine hosts input
// for any duplicate hostname return error
uniqueHostnames := make(map[string]bool)
createInput := *requests[0].Input
createInput.Hosts = nil
for _, req := range requests {
for _, host := range req.Input.Hosts {
if _, ok := uniqueHostnames[host.Hostname]; ok {
result := Result{
Error: fmt.Errorf("duplicate hostname found: %s", host.Hostname),
}
req.ResultChan <- result
continue
}
uniqueHostnames[host.Hostname] = true
createInput.Hosts = append(createInput.Hosts, host)
}
}

// create servers
dedicatedServers, err := client.Hosts.CreateDedicatedServers(context.TODO(), createInput)
result := Result{
Servers: dedicatedServers,
Error: err,
}

for _, req := range requests {
req.ResultChan <- result
}
}

// calculateServerChecksum generate checksum for server create input excepting the Hosts field
func calculateServerChecksum(input scgo.DedicatedServerCreateInput) (string, error) {
input.Hosts = []scgo.DedicatedServerHostInput{}

data, err := json.Marshal(input)
if err != nil {
return "", err
}

hash := sha256.Sum256(data)
return hex.EncodeToString(hash[:]), nil
}

0 comments on commit 5cd899b

Please sign in to comment.