This replaces the start_time, end_time, and previous_end_time columns on
the session_state table with a time range column that records when the
state was active. This allowed for dropping several constraints on the
session_state table that were used to ensure that states for a session
did not overlap. Instead this can be ensured with a single exclusion
constraint. By reducing the number of constraints, it improves the
performance for a number of queries involved with session state. Most
notably when resolving delete cascades of resources related to sessions,
such as targets, where as part of the delete transaction large numbers
of sessions get canceled. Similarly, when sessions get deleted, and the
corresponding session_state rows are deleted, these constraints all must
be checked during the transaction.
The domain layer continues to expose StartTime and EndTime fields on
session.State. However it drops PreviousEndTime, which was never used by
anything calling the domain layer.
See:
https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-EXCLUSION
https://www.postgresql.org/docs/current/rangetypes.html
157 lines
4.1 KiB
Go
157 lines
4.1 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package session
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/hashicorp/boundary/internal/db"
|
|
"github.com/hashicorp/boundary/internal/db/timestamp"
|
|
"github.com/hashicorp/boundary/internal/errors"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
|
|
workerpbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services"
|
|
)
|
|
|
|
const (
|
|
defaultStateTableName = "session_state"
|
|
)
|
|
|
|
// Status of the session's state
|
|
type Status string
|
|
|
|
const (
|
|
StatusPending Status = "pending"
|
|
StatusActive Status = "active"
|
|
StatusCanceling Status = "canceling"
|
|
StatusTerminated Status = "terminated"
|
|
)
|
|
|
|
// String representation of the state's status
|
|
func (s Status) String() string {
|
|
return string(s)
|
|
}
|
|
|
|
// ProtoVal returns the enum value corresponding to the state
|
|
func (s Status) ProtoVal() workerpbs.SESSIONSTATUS {
|
|
switch s {
|
|
case StatusPending:
|
|
return workerpbs.SESSIONSTATUS_SESSIONSTATUS_PENDING
|
|
case StatusActive:
|
|
return workerpbs.SESSIONSTATUS_SESSIONSTATUS_ACTIVE
|
|
case StatusCanceling:
|
|
return workerpbs.SESSIONSTATUS_SESSIONSTATUS_CANCELING
|
|
case StatusTerminated:
|
|
return workerpbs.SESSIONSTATUS_SESSIONSTATUS_TERMINATED
|
|
}
|
|
return workerpbs.SESSIONSTATUS_SESSIONSTATUS_UNSPECIFIED
|
|
}
|
|
|
|
// State of the session
|
|
type State struct {
|
|
// SessionId references the session public id
|
|
SessionId string `json:"session_id,omitempty" gorm:"primary_key"`
|
|
// status of the session
|
|
Status Status `json:"status,omitempty" gorm:"column:state"`
|
|
// StartTime from the RDBMS
|
|
StartTime *timestamp.Timestamp `json:"start_time,omitempty" gorm:"default:current_timestamp;primary_key"`
|
|
// EndTime from the RDBMS
|
|
EndTime *timestamp.Timestamp `json:"end_time,omitempty" gorm:"default:current_timestamp"`
|
|
|
|
tableName string `gorm:"-"`
|
|
}
|
|
|
|
var (
|
|
_ Cloneable = (*State)(nil)
|
|
_ db.VetForWriter = (*State)(nil)
|
|
)
|
|
|
|
// NewState creates a new in memory session state. No options
|
|
// are currently supported.
|
|
func NewState(ctx context.Context, session_id string, state Status, _ ...Option) (*State, error) {
|
|
const op = "session.NewState"
|
|
s := State{
|
|
SessionId: session_id,
|
|
Status: state,
|
|
}
|
|
|
|
if err := s.validate(ctx); err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
return &s, nil
|
|
}
|
|
|
|
// allocState will allocate a State
|
|
func allocState() State {
|
|
return State{}
|
|
}
|
|
|
|
// Clone creates a clone of the State
|
|
func (s *State) Clone() any {
|
|
clone := &State{
|
|
SessionId: s.SessionId,
|
|
Status: s.Status,
|
|
}
|
|
if s.StartTime != nil {
|
|
clone.StartTime = ×tamp.Timestamp{
|
|
Timestamp: ×tamppb.Timestamp{
|
|
Seconds: s.StartTime.Timestamp.Seconds,
|
|
Nanos: s.StartTime.Timestamp.Nanos,
|
|
},
|
|
}
|
|
}
|
|
if s.EndTime != nil {
|
|
clone.EndTime = ×tamp.Timestamp{
|
|
Timestamp: ×tamppb.Timestamp{
|
|
Seconds: s.EndTime.Timestamp.Seconds,
|
|
Nanos: s.EndTime.Timestamp.Nanos,
|
|
},
|
|
}
|
|
}
|
|
return clone
|
|
}
|
|
|
|
// VetForWrite implements db.VetForWrite() interface and validates the state
|
|
// before it's written.
|
|
func (s *State) VetForWrite(ctx context.Context, _ db.Reader, _ db.OpType, _ ...db.Option) error {
|
|
const op = "session.(State).VetForWrite"
|
|
if err := s.validate(ctx); err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// TableName returns the tablename to override the default gorm table name
|
|
func (s *State) TableName() string {
|
|
if s.tableName != "" {
|
|
return s.tableName
|
|
}
|
|
return defaultStateTableName
|
|
}
|
|
|
|
// SetTableName sets the tablename and satisfies the ReplayableMessage
|
|
// interface. If the caller attempts to set the name to "" the name will be
|
|
// reset to the default name.
|
|
func (s *State) SetTableName(n string) {
|
|
s.tableName = n
|
|
}
|
|
|
|
// validate checks the session state
|
|
func (s *State) validate(ctx context.Context) error {
|
|
const op = "session.(State).validate"
|
|
if s.Status == "" {
|
|
return errors.New(ctx, errors.InvalidParameter, op, "missing status")
|
|
}
|
|
if s.SessionId == "" {
|
|
return errors.New(ctx, errors.InvalidParameter, op, "missing session id")
|
|
}
|
|
if s.StartTime != nil {
|
|
return errors.New(ctx, errors.InvalidParameter, op, "start time is not settable")
|
|
}
|
|
if s.EndTime != nil {
|
|
return errors.New(ctx, errors.InvalidParameter, op, "end time is not settable")
|
|
}
|
|
return nil
|
|
}
|