Files
threshold/internal/daemon/worker/session_info.go
T
Johan Brandhorst-SatzkornandDamian Debkowski ec0a3f964b Worker status refactor (#5417)
* 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>
2025-01-09 10:26:37 -08:00

230 lines
8.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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)
}