* oss: feat(proto): statistics rpc added to server coordination service * oss: feat(session): add update statistics status service * oss: feat(handlers): add Statistics method to worker service server * oss: feat(worker): add statistics ticking * oss: internal/proto: define the new RoutingInfo RPC * oss: internal/server: repository changes for RoutingInfo RPC * oss: internal/daemon: add RoutingInfo handler to worker service The new RoutingInfo RPC is used to get information from the worker that is used to make routing decisions on the controller. It also handles the initial request from the worker. * oss: feat(db): server_worker_session_info table * oss: feat(proto): session info rpc for server coordination service * oss: feat(server): worker session info request data type * oss: feat(worker): session info ticker * oss: feat(handler): server coordination handler for session info * oss: feat(session): utilize session info request time for session cleanup * oss: proto: fix some gotags comments Some of these fields were missing comments, while others had incorrectly formatted comments. * oss: worker: send connection status to server This is required for the correct tracking of orphaned connections * oss: internal/daemon/worker: add function to wait for Statistics RPC * oss: internal/daemon/worker: add routing info RPC The new RoutingInfo RPC replaces part of the Status RPC, namely the part which is used by the controller and worker to perform decisions around the establishment of new sessions and new worker connections. * oss: internal/daemon/worker: remove unnecessary error check * oss: internal/daemon/worker: lock access to config and upstreams The new confLock protects access to the conf value and to the addressReceivers slice. * oss: worker: replace Status RPC with RoutingInfo * oss: worker: deprecate the Status RPC and related types The Status RPC and related messages have been replaced by the RoutingInfo/SessionInfo/Statistics RPCs. The controller will continue to support the Status RPC until two new minor versions have been released. * oss: internal/event: add default deny for new worker RPCs * oss: internal/daemon/worker: allow RPC interval to be set The new SessionInfo/RoutingInfo/Statistics RPCs run at long intervals, which make some of our cluster tests take a long time to complete. This allows their interval to be configured in tests. * oss: all: use TestWorkerRPCInterval in slow tests This should speed up these tests significantly. * oss: internal/tests/cluster: reset sys eventer in test --------- Co-authored-by: Damian Debkowski <damian.debkowski@hashicorp.com>
230 lines
8.7 KiB
Go
230 lines
8.7 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
||
// SPDX-License-Identifier: BUSL-1.1
|
||
|
||
package worker
|
||
|
||
import (
|
||
"context"
|
||
stderr "errors"
|
||
"math/rand"
|
||
"time"
|
||
|
||
"github.com/hashicorp/boundary/internal/daemon/worker/session"
|
||
"github.com/hashicorp/boundary/internal/errors"
|
||
"github.com/hashicorp/boundary/internal/event"
|
||
pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services"
|
||
)
|
||
|
||
// lastSessionInfo holds the last successful session info RPC time.
|
||
type lastSessionInfo struct {
|
||
LastSuccessfulRequestTime time.Time
|
||
}
|
||
|
||
func (w *Worker) startSessionInfoTicking(cancelCtx context.Context) {
|
||
const op = "worker.(Worker).startSessionInfoTicking"
|
||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||
timer := time.NewTimer(0)
|
||
for {
|
||
select {
|
||
case <-cancelCtx.Done():
|
||
event.WriteSysEvent(w.baseContext, op, "session infor ticking shutting down")
|
||
return
|
||
case <-timer.C:
|
||
err := w.sendSessionInfo(cancelCtx)
|
||
if err != nil {
|
||
event.WriteError(w.baseContext, op, err)
|
||
}
|
||
// Add a bit of jitter to the wait, so we aren't always getting,
|
||
// session information updates at the exact same intervals, to
|
||
// ease the load on the DB.
|
||
timer.Reset(w.sessionInfoInterval + getRandomInterval(r))
|
||
}
|
||
}
|
||
}
|
||
|
||
func (w *Worker) sendSessionInfo(cancelCtx context.Context) error {
|
||
const op = "worker.(Worker).sendSessionInfo"
|
||
// skip when the workerId is not available
|
||
if w.LastRoutingInfoSuccess() == nil {
|
||
return errors.New(cancelCtx, errors.Internal, op, "missing latest routing info")
|
||
}
|
||
workerId := w.LastRoutingInfoSuccess().GetWorkerId()
|
||
if workerId == "" {
|
||
return errors.New(cancelCtx, errors.Internal, op, "worker id is empty")
|
||
}
|
||
|
||
var recordedSessionIds []string
|
||
if w.recorderManager != nil {
|
||
recSessIds, err := w.recorderManager.SessionsManaged(cancelCtx)
|
||
if err != nil {
|
||
event.WriteError(cancelCtx, op, err, event.WithInfoMsg("error getting session ids from recorderManager"))
|
||
}
|
||
if len(recSessIds) != 0 {
|
||
recordedSessionIds = append(recordedSessionIds, recSessIds...)
|
||
}
|
||
}
|
||
|
||
var activeSessions []*pbs.Session
|
||
for _, sessionId := range recordedSessionIds {
|
||
activeSessions = append(activeSessions, &pbs.Session{
|
||
SessionId: sessionId,
|
||
SessionType: pbs.SessionType_SESSION_TYPE_RECORDED,
|
||
SessionStatus: pbs.SESSIONSTATUS_SESSIONSTATUS_ACTIVE,
|
||
})
|
||
}
|
||
w.sessionManager.ForEachLocalSession(func(s session.Session) bool {
|
||
activeSessions = append(activeSessions, &pbs.Session{
|
||
SessionId: s.GetId(),
|
||
SessionType: pbs.SessionType_SESSION_TYPE_INGRESSED,
|
||
SessionStatus: s.GetStatus(),
|
||
})
|
||
return true
|
||
})
|
||
// skip when there are no sessions to report
|
||
if len(activeSessions) == 0 {
|
||
return nil
|
||
}
|
||
clientCon := w.GrpcClientConn.Load()
|
||
client := pbs.NewServerCoordinationServiceClient(clientCon)
|
||
sessionInfoCtx, statusCancel := context.WithTimeout(cancelCtx, time.Duration(w.sessionInfoCallTimeoutDuration.Load()))
|
||
defer statusCancel()
|
||
result, err := client.SessionInfo(sessionInfoCtx, &pbs.SessionInfoRequest{
|
||
WorkerId: workerId,
|
||
Sessions: activeSessions,
|
||
})
|
||
if err != nil {
|
||
if isPastGrace, lastSessionInfoTime, gracePeriod := w.isPastSessionInfoGrace(); isPastGrace {
|
||
event.WriteError(cancelCtx, op,
|
||
stderr.New("session information error grace period has expired, canceling all sessions on worker"),
|
||
event.WithInfo("last_session_info_time", lastSessionInfoTime.String(), "grace_period", gracePeriod),
|
||
)
|
||
// 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.sessionManager.ForEachLocalSession(func(s session.Session) bool {
|
||
for _, connId := range s.CancelAllLocalConnections() {
|
||
event.WriteSysEvent(cancelCtx, op, "terminated connection due to session information grace period expiration", "session_id", s.GetId(), "connection_id", connId)
|
||
}
|
||
return true
|
||
})
|
||
}
|
||
return errors.Wrap(cancelCtx, err, op, errors.WithCode(errors.Internal), errors.WithMsg("error making session information request to controller: controller_address: %s", clientCon.Target()))
|
||
}
|
||
|
||
w.lastSessionInfoSuccess.Store(&lastSessionInfo{
|
||
LastSuccessfulRequestTime: time.Now(),
|
||
})
|
||
|
||
var nonActiveRecordedSessionIds []string
|
||
for _, s := range result.GetNonActiveSessions() {
|
||
switch s.GetSessionType() {
|
||
case pbs.SessionType_SESSION_TYPE_RECORDED:
|
||
if s.SessionStatus != pbs.SESSIONSTATUS_SESSIONSTATUS_ACTIVE {
|
||
nonActiveRecordedSessionIds = append(nonActiveRecordedSessionIds, s.GetSessionId())
|
||
}
|
||
case pbs.SessionType_SESSION_TYPE_INGRESSED:
|
||
sessionId := s.GetSessionId()
|
||
managedSession := w.sessionManager.Get(sessionId)
|
||
if managedSession == nil {
|
||
event.WriteError(cancelCtx, op, stderr.New("session change requested but could not find local information for it"), event.WithInfo("session_id", sessionId))
|
||
continue
|
||
}
|
||
managedSession.ApplyLocalStatus(s.GetSessionStatus())
|
||
// Update connection state if there are any connections in the request.
|
||
for _, c := range s.GetConnections() {
|
||
if err := managedSession.ApplyLocalConnectionStatus(c.GetConnectionId(), c.GetStatus()); err != nil {
|
||
event.WriteError(cancelCtx, op, err, event.WithInfo("connection_id", c.GetConnectionId()))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if w.recorderManager != nil {
|
||
if err := w.recorderManager.ReauthorizeAllExcept(cancelCtx, nonActiveRecordedSessionIds); err != nil {
|
||
event.WriteError(cancelCtx, op, err)
|
||
}
|
||
}
|
||
// Standard cleanup: Run through current jobs. Cancel connections
|
||
// for any canceling session or any session that is expired.
|
||
w.cleanupConnections(cancelCtx, false, w.sessionManager)
|
||
return nil
|
||
}
|
||
|
||
func (w *Worker) isPastSessionInfoGrace() (bool, time.Time, time.Duration) {
|
||
t := w.workerStartTime
|
||
info := w.lastSessionInfoSuccess.Load().(*lastSessionInfo)
|
||
if info != nil {
|
||
t = info.LastSuccessfulRequestTime
|
||
}
|
||
u := time.Duration(w.successfulSessionInfoGracePeriod.Load())
|
||
v := time.Since(t)
|
||
return v > u, t, u
|
||
}
|
||
|
||
// cleanupConnections walks all sessions and shuts down all proxy connections.
|
||
// After the local connections are terminated, they are requested to be marked
|
||
// closed on the controller.
|
||
// 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, sessionManager session.Manager) {
|
||
const op = "worker.(Worker).cleanupConnections"
|
||
closeInfo := make(map[string]*session.ConnectionCloseData)
|
||
cleanSessionIds := make([]string, 0)
|
||
sessionManager.ForEachLocalSession(func(s session.Session) bool {
|
||
switch {
|
||
case ignoreSessionState,
|
||
s.GetStatus() == pbs.SESSIONSTATUS_SESSIONSTATUS_CANCELING,
|
||
s.GetStatus() == pbs.SESSIONSTATUS_SESSIONSTATUS_TERMINATED,
|
||
time.Until(s.GetExpiration()) < 0:
|
||
// Cancel connections without regard to individual connection
|
||
// state.
|
||
closedIds := s.CancelAllLocalConnections()
|
||
localConns := s.GetLocalConnections()
|
||
for _, connId := range closedIds {
|
||
var bytesUp, bytesDown int64
|
||
connInfo, ok := localConns[connId]
|
||
if ok {
|
||
bytesUp = connInfo.BytesUp()
|
||
bytesDown = connInfo.BytesDown()
|
||
}
|
||
closeInfo[connId] = &session.ConnectionCloseData{
|
||
SessionId: s.GetId(),
|
||
BytesUp: bytesUp,
|
||
BytesDown: bytesDown,
|
||
}
|
||
event.WriteSysEvent(cancelCtx, op, "terminated connection due to cancellation or expiration", "session_id", s.GetId(), "connection_id", connId)
|
||
}
|
||
|
||
// If the session is no longer valid and all connections
|
||
// are marked closed, clean up the session.
|
||
if len(closedIds) == 0 {
|
||
cleanSessionIds = append(cleanSessionIds, s.GetId())
|
||
}
|
||
|
||
default:
|
||
// Cancel connections *with* regard to individual connection
|
||
// state (ie: only ones that the controller has requested be
|
||
// terminated).
|
||
for _, connId := range s.CancelOpenLocalConnections() {
|
||
closeInfo[connId] = &session.ConnectionCloseData{SessionId: s.GetId()}
|
||
event.WriteSysEvent(cancelCtx, op, "terminated connection due to cancellation or expiration", "session_id", s.GetId(), "connection_id", connId)
|
||
}
|
||
}
|
||
|
||
return true
|
||
})
|
||
|
||
// Note that we won't clean these from the info map until the
|
||
// next time we run this function
|
||
if len(closeInfo) > 0 {
|
||
// 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.
|
||
_ = sessionManager.RequestCloseConnections(cancelCtx, closeInfo)
|
||
}
|
||
// Forget sessions where the session is expired/canceled and all
|
||
// connections are canceled and marked closed
|
||
sessionManager.DeleteLocalSession(cleanSessionIds)
|
||
}
|