2020-08-12 00:40:39 -04:00
package worker
import (
"context"
2021-07-28 06:31:16 -04:00
"errors"
2020-08-12 00:40:39 -04:00
"math/rand"
"time"
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"
2020-08-12 00:40:39 -04:00
"github.com/hashicorp/boundary/internal/servers"
2021-08-18 15:30:55 +02:00
"github.com/hashicorp/boundary/internal/servers/worker/common"
"github.com/hashicorp/boundary/internal/servers/worker/session"
2020-08-12 00:40:39 -04:00
"github.com/hashicorp/boundary/internal/types/resource"
"google.golang.org/grpc/resolver"
)
2020-09-03 13:24:34 -04:00
type LastStatusInformation struct {
* pbs . StatusResponse
StatusTime time . Time
}
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"
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 )
var tags map [ string ] * servers . TagValues
// If we're not going to request a tag update, no reason to have these
// marshaled on every status call.
if w . updateTags . Load () {
tags = w . tags . Load ().( map [ string ] * servers . TagValues )
}
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 ()
result , err := client . Status ( statusCtx , & pbs . StatusRequest {
2021-01-21 16:27:20 -05:00
Jobs : activeJobs ,
Worker : & servers . Server {
PrivateId : w . conf . RawConfig . Worker . Name ,
Type : resource . Worker . String (),
Description : w . conf . RawConfig . Worker . Description ,
Address : w . conf . RawConfig . Worker . PublicAddr ,
Tags : tags ,
},
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
// want to start terminating all sessions as a "break glass" kind of
// 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
)
// Run a "cleanup" for all sessions that will not be caught by
// our standard cleanup routine.
w . cleanupConnections ( cancelCtx , true )
}
2021-01-21 16:27:20 -05:00
} else {
w . updateTags . Store ( false )
addrs := make ([] resolver . Address , 0 , len ( result . Controllers ))
strAddrs := make ([] string , 0 , len ( result . Controllers ))
for _ , v := range result . Controllers {
addrs = append ( addrs , resolver . Address { Addr : v . Address })
strAddrs = append ( strAddrs , v . Address )
}
switch len ( strAddrs ) {
case 0 :
2021-07-28 06:31:16 -04:00
event . WriteError ( statusCtx , op , errors . New ( "got no controller addresses from controller; possibly prior to first status save, not persisting" ))
2021-01-21 16:27:20 -05:00
default :
w . Resolver (). UpdateState ( resolver . State { Addresses : addrs })
}
w . lastStatusSuccess . Store ( & LastStatusInformation { StatusResponse : result , StatusTime : time . Now ()})
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
}