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-06-23 12:16:16 -07:00
pb "github.com/hashicorp/boundary/internal/gen/controller/servers"
2022-06-20 18:20:17 -07:00
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:42:56 -04:00
"github.com/hashicorp/go-secure-stdlib/strutil"
2020-08-12 00:40:39 -04:00
)
2022-08-03 19:29:17 -04:00
var firstStatusCheckPostHooks [] func ( context . Context , * Worker ) error
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
}
2022-07-25 12:52:21 -07:00
func ( w * Worker ) startStatusTicking ( cancelCtx context . Context , sessionManager session . Manager , addrReceivers [] addressReceiver ) {
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 :
2022-07-07 12:13:17 -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 skip calling the function and retry in a short duration
2022-08-03 19:29:17 -04:00
if w . everAuthenticated . Load () == authenticationStatusNeverAuthenticated {
2022-07-07 12:13:17 -04:00
timer . Reset ( 10 * time . Millisecond )
continue
}
2022-07-25 12:52:21 -07:00
w . sendWorkerStatus ( cancelCtx , sessionManager , addrReceivers )
2021-07-28 06:31:16 -04:00
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
}
2022-07-25 12:52:21 -07:00
func ( w * Worker ) sendWorkerStatus ( cancelCtx context . Context , sessionManager session . Manager , addressReceivers [] addressReceiver ) {
2021-07-28 06:31:16 -04:00
const op = "worker.(Worker).sendWorkerStatus"
2022-04-27 16:25:05 -04:00
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
2022-07-06 13:37:49 -07:00
sessionManager . ForEachLocalSession ( func ( s session . Session ) bool {
2021-01-21 16:27:20 -05:00
var jobInfo pbs . SessionJobInfo
2022-07-06 13:37:49 -07:00
status := s . GetStatus ()
sessionId := s . GetId ()
connections := make ([] * pbs . Connection , 0 , len ( s . GetLocalConnections ()))
for k , v := range s . GetLocalConnections () {
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
})
}
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-23 12:16:16 -07:00
var tags [] * pb . 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-23 12:16:16 -07:00
tags = w . tags . Load ().([] * pb . 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-23 12:16:16 -07:00
WorkerStatus : & pb . ServerWorkerStatus {
2022-06-15 15:50:36 -04:00
Name : w . conf . RawConfig . Worker . Name ,
Description : w . conf . RawConfig . Worker . Description ,
Address : w . conf . RawConfig . Worker . PublicAddr ,
Tags : tags ,
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.
2022-07-06 13:37:49 -07:00
sessionManager . ForEachLocalSession ( func ( s session . Session ) bool {
for _ , connId := range s . CancelAllLocalConnections () {
event . WriteSysEvent ( cancelCtx , op , "terminated connection due to status grace period expiration" , "session_id" , s . GetId (), "connection_id" , connId )
2022-03-09 13:16:52 -05:00
}
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
}
2022-08-03 19:29:17 -04:00
// Standard cleanup: Run through current jobs. Cancel connections
// for any canceling session or any session that is expired.
w . cleanupConnections ( cancelCtx , false , sessionManager )
return
}
w . updateTags . Store ( false )
var addrs [] string
// This may be empty if we are in a multiple hop scenario
if len ( result . CalculatedUpstreams ) > 0 {
addrs = make ([] string , 0 , len ( result . CalculatedUpstreams ))
for _ , v := range result . CalculatedUpstreams {
addrs = append ( addrs , v . Address )
}
} else if w . conf . RawConfig . HcpbClusterId != "" {
// This is a worker that is one hop away from managed workers, so attempt to get that list
hcpbWorkersCtx , hcpbWorkersCancel := context . WithTimeout ( cancelCtx , common . StatusTimeout )
defer hcpbWorkersCancel ()
workersResp , err := client . ListHcpbWorkers ( hcpbWorkersCtx , & pbs . ListHcpbWorkersRequest {})
if err != nil {
event . WriteError ( hcpbWorkersCtx , op , err , event . WithInfoMsg ( "error fetching managed worker information" ))
} else {
addrs = make ([] string , 0 , len ( workersResp . Workers ))
for _ , v := range workersResp . Workers {
2022-07-25 12:52:21 -07:00
addrs = append ( addrs , v . Address )
2022-06-01 01:09:56 -04:00
}
2022-08-03 19:29:17 -04:00
}
}
2022-07-07 19:02:16 -04:00
2022-08-03 19:29:17 -04:00
if len ( addrs ) > 0 {
lastStatus := w . lastStatusSuccess . Load ().( * LastStatusInformation )
// Compare upstreams; update resolver if there is a difference, and emit an event with old and new addresses
if lastStatus != nil && ! strutil . EquivalentSlices ( lastStatus . LastCalculatedUpstreams , addrs ) {
upstreamsMessage := fmt . Sprintf ( "Upstreams has changed; old upstreams were: %s, new upstreams are: %s" , lastStatus . LastCalculatedUpstreams , addrs )
event . WriteSysEvent ( cancelCtx , op , upstreamsMessage )
for _ , as := range addressReceivers {
as . SetAddresses ( addrs )
2022-07-07 19:02:16 -04:00
}
2022-08-03 19:29:17 -04:00
} else if lastStatus == nil {
for _ , as := range addressReceivers {
as . SetAddresses ( addrs )
2022-06-01 01:09:56 -04:00
}
2022-08-03 19:29:17 -04:00
event . WriteSysEvent ( cancelCtx , op , fmt . Sprintf ( "Upstreams after first status set to: %s" , addrs ))
2021-01-21 16:27:20 -05:00
}
2022-08-03 19:29:17 -04:00
}
w . lastStatusSuccess . Store ( & LastStatusInformation { StatusResponse : result , StatusTime : time . Now (), LastCalculatedUpstreams : addrs })
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 ()
si := sessionManager . Get ( sessionId )
if si == nil {
event . WriteError ( statusCtx , op , errors . New ( "session change requested but could not find local information for it" ), event . WithInfo ( "session_id" , sessionId ))
continue
}
si . ApplyLocalStatus ( sessInfo . GetStatus ())
// Update connection state if there are any connections in
// the request.
for _ , conn := range sessInfo . GetConnections () {
if err := si . ApplyLocalConnectionStatus ( conn . GetConnectionId (), conn . GetStatus ()); err != nil {
event . WriteError ( statusCtx , op , err , event . WithInfo ( "connection_id" , conn . GetConnectionId ()))
2021-07-14 09:14:24 -07:00
}
2020-09-21 02:22:59 -04:00
}
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.
2022-07-06 13:37:49 -07:00
w . cleanupConnections ( cancelCtx , false , sessionManager )
2022-08-03 19:29:17 -04:00
// If we have post hooks for after the first status check, run them now
if w . everAuthenticated . CAS ( authenticationStatusFirstAuthentication , authenticationStatusFirstStatusRpcSuccessful ) {
for _ , fn := range firstStatusCheckPostHooks {
if err := fn ( cancelCtx , w ); err != nil {
// If we can't verify status we can't be expected to behave
// properly so error and trigger shutdown
event . WriteError ( cancelCtx , op , fmt . Errorf ( "error running first status check post hook: %w" , err ))
// We don't use a non-blocking select here to ensure that it
// happens; we should catch blocks in tests but we want to
// ensure the signal is being listened to
w . conf . ServerSideShutdownCh <- struct {}{}
return
}
}
}
2021-06-28 17:32:13 -07:00
}
2022-07-06 13:37:49 -07:00
// cleanupConnections walks all sessions and shuts down all proxy connections.
// After the local connections are terminated, they are requested to be marked
// close on the controller.
2021-06-28 17:32:13 -07:00
// Additionally, sessions without connections are cleaned up from the
// local worker's state.
//
// Use ignoreSessionState to ignore the state checks, this closes all
2022-07-06 13:37:49 -07:00
// connections, regardless of whether or not the session is still active.
func ( w * Worker ) cleanupConnections ( cancelCtx context . Context , ignoreSessionState bool , sessionManager session . Manager ) {
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 )
2022-07-06 13:37:49 -07:00
sessionManager . ForEachLocalSession ( func ( s session . Session ) bool {
2021-01-21 16:27:20 -05:00
switch {
2021-06-28 17:32:13 -07:00
case ignoreSessionState ,
2022-07-06 13:37:49 -07:00
s . GetStatus () == pbs . SESSIONSTATUS_SESSIONSTATUS_CANCELING ,
s . GetStatus () == pbs . SESSIONSTATUS_SESSIONSTATUS_TERMINATED ,
time . Until ( s . GetExpiration ()) < 0 :
2021-07-14 09:14:24 -07:00
// Cancel connections without regard to individual connection
// state.
2022-07-06 13:37:49 -07:00
closedIds := s . CancelAllLocalConnections ()
2021-07-14 09:14:24 -07:00
for _ , connId := range closedIds {
2022-07-06 13:37:49 -07:00
closeInfo [ connId ] = s . GetId ()
event . WriteSysEvent ( cancelCtx , op , "terminated connection due to cancellation or expiration" , "session_id" , s . GetId (), "connection_id" , connId )
2021-01-21 16:27:20 -05:00
}
2021-07-14 09:14:24 -07:00
2022-07-06 13:37:49 -07:00
// If the session is no longer valid and all connections
2021-01-21 16:27:20 -05:00
// are marked closed, clean up the session.
2021-07-14 09:14:24 -07:00
if len ( closedIds ) == 0 {
2022-07-06 13:37:49 -07:00
cleanSessionIds = append ( cleanSessionIds , s . GetId ())
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).
2022-07-06 13:37:49 -07:00
for _ , connId := range s . CancelOpenLocalConnections () {
closeInfo [ connId ] = s . GetId ()
event . WriteSysEvent ( cancelCtx , op , "terminated connection due to cancellation or expiration" , "session_id" , s . GetId (), "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.
2022-07-06 13:37:49 -07:00
_ = sessionManager . RequestCloseConnections ( cancelCtx , 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
2022-07-06 13:37:49 -07:00
sessionManager . DeleteLocalSession ( cleanSessionIds )
2021-07-14 09:14:24 -07:00
}
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
}