2020-08-12 00:40:39 -04:00
package worker
import (
"context"
2021-07-28 06:31:16 -04:00
"errors"
2022-06-09 16:04:13 -04:00
"fmt"
2020-08-12 00:40:39 -04:00
"math/rand"
"time"
2022-05-09 12:45:10 -07:00
"github.com/hashicorp/boundary/internal/daemon/worker/common"
"github.com/hashicorp/boundary/internal/daemon/worker/session"
2020-09-12 21:54:18 -07:00
pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services"
2021-07-28 06:31:16 -04:00
"github.com/hashicorp/boundary/internal/observability/event"
2022-06-09 16:04:13 -04:00
"github.com/hashicorp/boundary/internal/servers"
2022-06-09 16:42:56 -04:00
"github.com/hashicorp/go-secure-stdlib/strutil"
2020-08-12 00:40:39 -04:00
"google.golang.org/grpc/resolver"
)
2020-09-03 13:24:34 -04:00
type LastStatusInformation struct {
* pbs . StatusResponse
2022-06-09 16:04:13 -04:00
StatusTime time . Time
2022-06-09 16:39:20 -04:00
LastCalculatedUpstreams [] string
2020-09-03 13:24:34 -04:00
}
2020-08-12 00:40:39 -04:00
func ( w * Worker ) startStatusTicking ( cancelCtx context . Context ) {
2021-07-28 06:31:16 -04:00
const op = "worker.(Worker).startStatusTicking"
r := rand . New ( rand . NewSource ( time . Now (). UnixNano ()))
// This function exists to desynchronize calls to controllers from
2021-08-18 15:30:55 +02:00
// workers, so we aren't always getting status updates at the exact same
2021-07-28 06:31:16 -04:00
// intervals, to ease the load on the DB.
getRandomInterval := func () time . Duration {
// 0 to 0.5 adjustment to the base
f := r . Float64 () / 2
// Half a chance to be faster, not slower
if r . Float32 () > 0.5 {
f = - 1 * f
2020-08-12 00:40:39 -04:00
}
2021-08-18 15:30:55 +02:00
return common . StatusInterval + time . Duration ( f * float64 ( time . Second ))
2021-07-28 06:31:16 -04:00
}
2020-08-12 00:40:39 -04:00
2021-07-28 06:31:16 -04:00
timer := time . NewTimer ( 0 )
for {
select {
case <- cancelCtx . Done ():
2021-08-18 15:30:55 +02:00
event . WriteSysEvent ( w . baseContext , op , "status ticking shutting down" )
2021-07-28 06:31:16 -04:00
return
2020-08-12 00:40:39 -04:00
2021-07-28 06:31:16 -04:00
case <- timer . C :
w . sendWorkerStatus ( cancelCtx )
timer . Reset ( getRandomInterval ())
2021-01-21 16:27:20 -05:00
}
2021-07-28 06:31:16 -04:00
}
2021-01-21 16:27:20 -05:00
}
2021-06-28 17:32:13 -07:00
// LastStatusSuccess reports the last time we sent a successful
// status request.
2021-01-21 16:27:20 -05:00
func ( w * Worker ) LastStatusSuccess () * LastStatusInformation {
return w . lastStatusSuccess . Load ().( * LastStatusInformation )
}
2021-07-14 09:14:24 -07:00
// WaitForNextSuccessfulStatusUpdate waits for the next successful status. It's
// used by testing (and in the future, shutdown) in place of a more opaque and
// possibly unnecessarily long sleep for things like initial controller
// check-in, etc.
//
// The timeout is aligned with the worker's status grace period. A nil error
// means the status was sent successfully.
func ( w * Worker ) WaitForNextSuccessfulStatusUpdate () error {
2021-07-28 06:31:16 -04:00
const op = "worker.(Worker).WaitForNextSuccessfulStatusUpdate"
2021-07-14 09:14:24 -07:00
waitStatusStart := time . Now ()
ctx , cancel := context . WithTimeout ( w . baseContext , w . conf . StatusGracePeriodDuration )
defer cancel ()
2021-07-28 06:31:16 -04:00
event . WriteSysEvent ( ctx , op , "waiting for next status report to controller" )
2021-07-14 09:14:24 -07:00
for {
select {
case <- time . After ( time . Second ):
// pass
case <- ctx . Done ():
2021-07-28 06:31:16 -04:00
event . WriteError ( ctx , op , ctx . Err (), event . WithInfoMsg ( "error waiting for next status report to controller" ))
2021-07-14 09:14:24 -07:00
return ctx . Err ()
}
if w . lastSuccessfulStatusTime (). Sub ( waitStatusStart ) > 0 {
break
}
}
2021-07-28 06:31:16 -04:00
event . WriteSysEvent ( ctx , op , "next worker status update sent successfully" )
2021-07-14 09:14:24 -07:00
return nil
}
2021-01-21 16:27:20 -05:00
func ( w * Worker ) sendWorkerStatus ( cancelCtx context . Context ) {
2021-07-28 06:31:16 -04:00
const op = "worker.(Worker).sendWorkerStatus"
2022-04-27 16:25:05 -04:00
// If we've never managed to successfully authenticate then we won't have
// any session information anyways and this will produce a ton of noise in
// observability, so suppress it
if ! w . everAuthenticated . Load () {
2022-06-09 11:28:12 -04:00
event . WriteSysEvent ( cancelCtx , op , "worker is not authenticated to an upstream, not sending status" )
2022-04-27 16:25:05 -04:00
return
}
2021-01-21 16:27:20 -05:00
// First send info as-is. We'll perform cleanup duties after we
// get cancel/job change info back.
var activeJobs [] * pbs . JobStatus
// Range over known sessions and collect info
w . sessionInfoMap . Range ( func ( key , value interface {}) bool {
var jobInfo pbs . SessionJobInfo
sessionId := key .( string )
2021-08-18 15:30:55 +02:00
si := value .( * session . Info )
2021-01-21 16:27:20 -05:00
si . RLock ()
2021-08-18 15:30:55 +02:00
status := si . Status
connections := make ([] * pbs . Connection , 0 , len ( si . ConnInfoMap ))
for k , v := range si . ConnInfoMap {
2021-01-21 16:27:20 -05:00
connections = append ( connections , & pbs . Connection {
ConnectionId : k ,
2021-08-18 15:30:55 +02:00
Status : v . Status ,
2021-01-21 16:27:20 -05:00
})
}
si . RUnlock ()
jobInfo . SessionId = sessionId
activeJobs = append ( activeJobs , & pbs . JobStatus {
Job : & pbs . Job {
Type : pbs . JOBTYPE_JOBTYPE_SESSION ,
JobInfo : & pbs . Job_SessionInfo {
SessionInfo : & pbs . SessionJobInfo {
SessionId : sessionId ,
Status : status ,
Connections : connections ,
2020-09-03 13:24:34 -04:00
},
2021-01-21 16:27:20 -05:00
},
},
})
return true
})
2020-09-03 13:24:34 -04:00
2021-01-21 16:27:20 -05:00
// Send status information
client := w . controllerStatusConn . Load ().( pbs . ServerCoordinationServiceClient )
2022-06-03 11:59:02 -07:00
var tags [] * servers . TagPair
2021-01-21 16:27:20 -05:00
// If we're not going to request a tag update, no reason to have these
// marshaled on every status call.
if w . updateTags . Load () {
2022-06-03 11:59:02 -07:00
tags = w . tags . Load ().([] * servers . TagPair )
2021-01-21 16:27:20 -05:00
}
2021-08-18 15:30:55 +02:00
statusCtx , statusCancel := context . WithTimeout ( cancelCtx , common . StatusTimeout )
2021-06-28 17:32:13 -07:00
defer statusCancel ()
2022-05-02 10:32:33 -04:00
2022-06-07 16:43:07 -04:00
keyId := w . WorkerAuthCurrentKeyId . Load ()
2022-06-06 14:25:14 -04:00
if w . conf . RawConfig . Worker . Name == "" && keyId == "" {
event . WriteError ( statusCtx , op , errors . New ( "worker name and keyId are both empty; one is needed to identify a worker" ),
event . WithInfoMsg ( "error making status request to controller" ))
}
2021-06-28 17:32:13 -07:00
result , err := client . Status ( statusCtx , & pbs . StatusRequest {
2021-01-21 16:27:20 -05:00
Jobs : activeJobs ,
2022-06-03 11:59:02 -07:00
WorkerStatus : & servers . ServerWorkerStatus {
Name : w . conf . RawConfig . Worker . Name ,
Address : w . conf . RawConfig . Worker . PublicAddr ,
Tags : tags ,
2022-06-06 14:25:14 -04:00
KeyId : keyId ,
2021-01-21 16:27:20 -05:00
},
UpdateTags : w . updateTags . Load (),
})
if err != nil {
2021-07-28 06:31:16 -04:00
event . WriteError ( statusCtx , op , err , event . WithInfoMsg ( "error making status request to controller" ))
2021-06-28 17:32:13 -07:00
// Check for last successful status. Ignore nil last status, this probably
// means that we've never connected to a controller, and as such probably
// don't have any sessions to worry about anyway.
//
// If a length of time has passed since we've been able to communicate, we
2022-03-09 13:16:52 -05:00
// want to start terminating all connections as a "break glass" kind of
2021-06-28 17:32:13 -07:00
// scenario, as there will be no way we can really tell if these
// connections should continue to exist.
if isPastGrace , lastStatusTime , gracePeriod := w . isPastGrace (); isPastGrace {
2021-07-28 06:31:16 -04:00
event . WriteError ( statusCtx , op ,
errors . New ( "status error grace period has expired, canceling all sessions on worker" ),
event . WithInfo ( "last_status_time" , lastStatusTime . String (), "grace_period" , gracePeriod ),
2021-06-28 17:32:13 -07:00
)
2022-03-09 13:16:52 -05:00
// Cancel connections if grace period has expired. These Connections will be closed in the
// database on the next successful status report, or via the Controller’ s dead Worker cleanup connections job.
w . sessionInfoMap . Range ( func ( key , value interface {}) bool {
si := value .( * session . Info )
si . Lock ()
defer si . Unlock ()
closedIds := w . cancelConnections ( si . ConnInfoMap , true )
for _ , connId := range closedIds {
event . WriteSysEvent ( cancelCtx , op , "terminated connection due to status grace period expiration" , "session_id" , si . Id , "connection_id" , connId )
}
return true
})
// Exit out of status function; our work here is done and we don't need to create closeConnection requests
return
2021-06-28 17:32:13 -07:00
}
2021-01-21 16:27:20 -05:00
} else {
w . updateTags . Store ( false )
2022-06-01 01:09:56 -04:00
// This may be nil if we are in a multiple hop scenario
2022-06-09 16:04:13 -04:00
var addrs [] resolver . Address
2022-06-09 16:39:20 -04:00
var newUpstreams [] string
2022-06-03 11:59:02 -07:00
if len ( result . CalculatedUpstreams ) > 0 {
2022-06-09 16:04:13 -04:00
addrs = make ([] resolver . Address , 0 , len ( result . CalculatedUpstreams ))
2022-06-03 11:59:02 -07:00
for _ , v := range result . CalculatedUpstreams {
2022-06-01 01:09:56 -04:00
addrs = append ( addrs , resolver . Address { Addr : v . Address })
2022-06-09 16:39:20 -04:00
newUpstreams = append ( newUpstreams , v . Address )
2022-06-01 01:09:56 -04:00
}
2022-06-09 16:04:13 -04:00
lastStatus := w . lastStatusSuccess . Load ().( * LastStatusInformation )
// Compare upstreams; update resolver if there is a difference, and emit an event with old and new addresses
2022-06-09 16:39:20 -04:00
if lastStatus != nil && ! strutil . EquivalentSlices ( lastStatus . LastCalculatedUpstreams , newUpstreams ) {
upstreamsMessage := fmt . Sprintf ( "Upstreams has changed; old upstreams were: %s, new upstreams are: %s" , lastStatus . LastCalculatedUpstreams , newUpstreams )
2022-06-10 14:10:05 -04:00
event . WriteSysEvent ( cancelCtx , op , upstreamsMessage )
2022-06-09 16:04:13 -04:00
w . Resolver (). UpdateState ( resolver . State { Addresses : addrs })
2022-06-09 16:39:20 -04:00
} else if lastStatus == nil {
w . Resolver (). UpdateState ( resolver . State { Addresses : addrs })
2022-06-10 14:10:05 -04:00
event . WriteSysEvent ( cancelCtx , op , fmt . Sprintf ( "Upstreams after first status set to: %s" , newUpstreams ))
2022-06-01 01:09:56 -04:00
}
2021-01-21 16:27:20 -05:00
}
2022-06-09 16:39:20 -04:00
w . lastStatusSuccess . Store ( & LastStatusInformation { StatusResponse : result , StatusTime : time . Now (), LastCalculatedUpstreams : newUpstreams })
2020-09-21 02:22:59 -04:00
2021-01-21 16:27:20 -05:00
for _ , request := range result . GetJobsRequests () {
switch request . GetRequestType () {
case pbs . CHANGETYPE_CHANGETYPE_UPDATE_STATE :
switch request . GetJob (). GetType () {
case pbs . JOBTYPE_JOBTYPE_SESSION :
sessInfo := request . GetJob (). GetSessionInfo ()
sessionId := sessInfo . GetSessionId ()
siRaw , ok := w . sessionInfoMap . Load ( sessionId )
if ! ok {
2021-07-28 06:31:16 -04:00
event . WriteError ( statusCtx , op , errors . New ( "session change requested but could not find local information for it" ), event . WithInfo ( "session_id" , sessionId ))
2021-01-21 16:27:20 -05:00
continue
2020-09-21 02:22:59 -04:00
}
2021-08-18 15:30:55 +02:00
si := siRaw .( * session . Info )
2021-01-21 16:27:20 -05:00
si . Lock ()
2021-08-18 15:30:55 +02:00
si . Status = sessInfo . GetStatus ()
2021-07-14 09:14:24 -07:00
// Update connection state if there are any connections in
// the request.
for _ , conn := range sessInfo . GetConnections () {
connId := conn . GetConnectionId ()
2021-08-18 15:30:55 +02:00
connInfo , ok := si . ConnInfoMap [ connId ]
2021-07-14 09:14:24 -07:00
if ! ok {
2021-07-28 06:31:16 -04:00
event . WriteError ( statusCtx , op , errors . New ( "connection change requested but could not find local information for it" ), event . WithInfo ( "connection_id" , connId ))
2021-07-14 09:14:24 -07:00
continue
}
2021-08-18 15:30:55 +02:00
connInfo . Status = conn . GetStatus ()
2021-07-14 09:14:24 -07:00
}
2020-09-21 02:22:59 -04:00
si . Unlock ()
}
2021-01-21 16:27:20 -05:00
}
}
}
2020-09-21 02:22:59 -04:00
2021-06-28 17:32:13 -07:00
// Standard cleanup: Run through current jobs. Cancel connections
// for any canceling session or any session that is expired.
w . cleanupConnections ( cancelCtx , false )
}
// cleanupConnections walks all sessions and shuts down connections.
// Additionally, sessions without connections are cleaned up from the
// local worker's state.
//
// Use ignoreSessionState to ignore the state checks, this closes all
// connections, regardless of whether or not the session is still
// active.
func ( w * Worker ) cleanupConnections ( cancelCtx context . Context , ignoreSessionState bool ) {
2021-07-28 06:31:16 -04:00
const op = "worker.(Worker).cleanupConnections"
2021-01-21 16:27:20 -05:00
closeInfo := make ( map [ string ] string )
cleanSessionIds := make ([] string , 0 )
w . sessionInfoMap . Range ( func ( key , value interface {}) bool {
2021-08-18 15:30:55 +02:00
si := value .( * session . Info )
2021-01-21 16:27:20 -05:00
si . Lock ()
2021-06-28 17:32:13 -07:00
defer si . Unlock ()
2021-01-21 16:27:20 -05:00
switch {
2021-06-28 17:32:13 -07:00
case ignoreSessionState ,
2021-08-18 15:30:55 +02:00
si . Status == pbs . SESSIONSTATUS_SESSIONSTATUS_CANCELING ,
si . Status == pbs . SESSIONSTATUS_SESSIONSTATUS_TERMINATED ,
time . Until ( si . LookupSessionResponse . Expiration . AsTime ()) < 0 :
2021-07-14 09:14:24 -07:00
// Cancel connections without regard to individual connection
// state.
2021-08-18 15:30:55 +02:00
closedIds := w . cancelConnections ( si . ConnInfoMap , true )
2021-07-14 09:14:24 -07:00
for _ , connId := range closedIds {
2021-08-18 15:30:55 +02:00
closeInfo [ connId ] = si . Id
event . WriteSysEvent ( cancelCtx , op , "terminated connection due to cancellation or expiration" , "session_id" , si . Id , "connection_id" , connId )
2021-01-21 16:27:20 -05:00
}
2021-07-14 09:14:24 -07:00
2021-08-18 15:30:55 +02:00
// CloseTime is marked by CloseConnections iff the
2021-01-21 16:27:20 -05:00
// status is returned for that connection as closed. If
// the session is no longer valid and all connections
// are marked closed, clean up the session.
2021-07-14 09:14:24 -07:00
if len ( closedIds ) == 0 {
2021-08-18 15:30:55 +02:00
cleanSessionIds = append ( cleanSessionIds , si . Id )
2020-08-12 00:40:39 -04:00
}
2021-07-14 09:14:24 -07:00
default :
// Cancel connections *with* regard to individual connection
// state (ie: only ones that the controller has requested be
// terminated).
2021-08-18 15:30:55 +02:00
closedIds := w . cancelConnections ( si . ConnInfoMap , false )
2021-07-14 09:14:24 -07:00
for _ , connId := range closedIds {
2021-08-18 15:30:55 +02:00
closeInfo [ connId ] = si . Id
event . WriteSysEvent ( cancelCtx , op , "terminated connection due to cancellation or expiration" , "session_id" , si . Id , "connection_id" , connId )
2021-07-14 09:14:24 -07:00
}
2020-08-12 00:40:39 -04:00
}
2021-06-28 17:32:13 -07:00
2021-01-21 16:27:20 -05:00
return true
})
2020-08-12 00:40:39 -04:00
2021-01-21 16:27:20 -05:00
// Note that we won't clean these from the info map until the
// next time we run this function
if len ( closeInfo ) > 0 {
2021-06-28 17:32:13 -07:00
// Call out to a helper to send the connection close requests to the
// controller, and set the close time. This functionality is shared with
// post-close functionality in the proxy handler.
2021-08-18 15:30:55 +02:00
sessClient , err := w . ControllerSessionConn ()
if err != nil {
event . WriteError ( cancelCtx , op , err , event . WithInfo ( "failed to create controller session client, connections won't be cleaned up" ))
} else {
session . CloseConnections ( cancelCtx , sessClient , w . sessionInfoMap , closeInfo )
}
2021-01-21 16:27:20 -05:00
}
// Forget sessions where the session is expired/canceled and all
// connections are canceled and marked closed
for _ , v := range cleanSessionIds {
w . sessionInfoMap . Delete ( v )
}
2020-08-12 00:40:39 -04:00
}
2021-06-28 17:32:13 -07:00
2021-07-14 09:14:24 -07:00
// cancelConnections is run by cleanupConnections to iterate over a
2021-08-18 15:30:55 +02:00
// session's ConnInfoMap and close connections based on the
2021-07-14 09:14:24 -07:00
// connection's state (or regardless if ignoreConnectionState is
// set).
//
// The returned map and slice are the maps of connection -> session,
// and sessions to completely remove from local state, respectively.
2021-08-18 15:30:55 +02:00
func ( w * Worker ) cancelConnections ( connInfoMap map [ string ] * session . ConnInfo , ignoreConnectionState bool ) [] string {
2021-07-14 09:14:24 -07:00
var closedIds [] string
for k , v := range connInfoMap {
2021-08-18 15:30:55 +02:00
if v . CloseTime . IsZero () {
if ! ignoreConnectionState && v . Status != pbs . CONNECTIONSTATUS_CONNECTIONSTATUS_CLOSED {
2021-07-14 09:14:24 -07:00
continue
}
2021-08-18 15:30:55 +02:00
v . ConnCancel ()
2021-07-14 09:14:24 -07:00
closedIds = append ( closedIds , k )
}
}
return closedIds
}
2021-06-28 17:32:13 -07:00
func ( w * Worker ) lastSuccessfulStatusTime () time . Time {
lastStatus := w . LastStatusSuccess ()
if lastStatus == nil {
return w . workerStartTime
}
return lastStatus . StatusTime
}
func ( w * Worker ) isPastGrace () ( bool , time . Time , time . Duration ) {
t := w . lastSuccessfulStatusTime ()
2021-07-14 09:14:24 -07:00
u := w . conf . StatusGracePeriodDuration
2021-06-28 17:32:13 -07:00
v := time . Since ( t )
return v > u , t , u
}