Skip to content

Commit d15725c

Browse files
feat(exec): Add --no-session flag for improved performance
Fixes: #26588 For use cases like HPC, where `podman exec` is called in rapid succession, the standard exec process can become a bottleneck due to container locking and database I/O for session tracking. This commit introduces a new `--no-session` flag to `podman exec`. When used, this flag invokes a new, lightweight backend implementation that: - Skips container locking, reducing lock contention - Bypasses the creation, tracking, and removal of exec sessions in the database - Executes the command directly and retrieves the exit code without persisting session state - Maintains consistency with regular exec for container lookup, TTY handling, and environment setup - Shares implementation with health check execution to avoid code duplication The implementation addresses all performance bottlenecks while preserving compatibility with existing exec functionality including --latest flag support and proper exit code handling. Changes include: - Add --no-session flag to cmd/podman/containers/exec.go - Implement lightweight execution path in libpod/container_exec.go - Ensure consistent container validation and environment setup - Add comprehensive exit code testing including signal handling (exit 137) - Optimize configuration to skip unnecessary exit command setup Signed-off-by: Ryan McCann <ryan_mccann@student.uml.edu> Signed-off-by: ryanmccann1024 <ryan_mccann@student.uml.edu>
1 parent da88a5b commit d15725c

File tree

8 files changed

+236
-105
lines changed

8 files changed

+236
-105
lines changed

cmd/podman/containers/exec.go

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ var (
5151
execOpts entities.ExecOptions
5252
execDetach bool
5353
execCidFile string
54+
execNoSession bool
5455
)
5556

5657
func execFlags(cmd *cobra.Command) {
@@ -100,6 +101,10 @@ func execFlags(cmd *cobra.Command) {
100101
flags.Int32(waitFlagName, 0, "Total seconds to wait for container to start")
101102
_ = flags.MarkHidden(waitFlagName)
102103

104+
if !registry.IsRemote() {
105+
flags.BoolVar(&execNoSession, "no-session", false, "Do not create a database session for the exec process")
106+
}
107+
103108
if registry.IsRemote() {
104109
_ = flags.MarkHidden("preserve-fds")
105110
}
@@ -121,6 +126,12 @@ func init() {
121126
}
122127

123128
func exec(cmd *cobra.Command, args []string) error {
129+
if execNoSession {
130+
if execDetach || cmd.Flags().Changed("detach-keys") {
131+
return errors.New("--no-session cannot be used with --detach or --detach-keys")
132+
}
133+
}
134+
124135
nameOrID, command, err := determineTargetCtrAndCmd(args, execOpts.Latest, execCidFile != "")
125136
if err != nil {
126137
return err
@@ -168,17 +179,21 @@ func exec(cmd *cobra.Command, args []string) error {
168179
}
169180
}
170181

171-
if !execDetach {
172-
streams := define.AttachStreams{}
173-
streams.OutputStream = os.Stdout
174-
streams.ErrorStream = os.Stderr
175-
if execOpts.Interactive {
176-
streams.InputStream = bufio.NewReader(os.Stdin)
177-
streams.AttachInput = true
178-
}
179-
streams.AttachOutput = true
180-
streams.AttachError = true
182+
streams := define.AttachStreams{}
183+
streams.OutputStream = os.Stdout
184+
streams.ErrorStream = os.Stderr
185+
if execOpts.Interactive {
186+
streams.InputStream = bufio.NewReader(os.Stdin)
187+
streams.AttachInput = true
188+
}
189+
streams.AttachOutput = true
190+
streams.AttachError = true
181191

192+
if execNoSession {
193+
exitCode, err := registry.ContainerEngine().ContainerExecNoSession(registry.Context(), nameOrID, execOpts, streams)
194+
registry.SetExitCode(exitCode)
195+
return err
196+
} else if !execDetach {
182197
exitCode, err := registry.ContainerEngine().ContainerExec(registry.Context(), nameOrID, execOpts, streams)
183198
registry.SetExitCode(exitCode)
184199
return err
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
####> This option file is used in:
2+
####> podman exec
3+
####> If file is edited, make sure the changes
4+
####> are applicable to all of those.
5+
#### **--no-session**
6+
7+
Do not create a database session for the exec process. This can improve performance but the exec session will not be visible to other podman commands.

docs/source/markdown/podman-exec.1.md.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ Start the exec session, but do not attach to it. The command runs in the backgro
3131

3232
@@option latest
3333

34+
@@option no-session
35+
3436
@@option preserve-fd
3537

3638
@@option preserve-fds

libpod/container_exec.go

Lines changed: 97 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -821,87 +821,7 @@ func (c *Container) ExecResize(sessionID string, newSize resize.TerminalSize) er
821821
}
822822

823823
func (c *Container) healthCheckExec(config *ExecConfig, timeout time.Duration, streams *define.AttachStreams) (int, error) {
824-
unlock := true
825-
if !c.batched {
826-
c.lock.Lock()
827-
defer func() {
828-
if unlock {
829-
c.lock.Unlock()
830-
}
831-
}()
832-
833-
if err := c.syncContainer(); err != nil {
834-
return -1, err
835-
}
836-
}
837-
838-
if err := c.verifyExecConfig(config); err != nil {
839-
return -1, err
840-
}
841-
842-
if !c.ensureState(define.ContainerStateRunning) {
843-
return -1, fmt.Errorf("can only create exec sessions on running containers: %w", define.ErrCtrStateInvalid)
844-
}
845-
846-
session, err := c.createExecSession(config)
847-
if err != nil {
848-
return -1, err
849-
}
850-
851-
if c.state.ExecSessions == nil {
852-
c.state.ExecSessions = make(map[string]*ExecSession)
853-
}
854-
c.state.ExecSessions[session.ID()] = session
855-
defer delete(c.state.ExecSessions, session.ID())
856-
857-
opts, err := prepareForExec(c, session)
858-
if err != nil {
859-
return -1, err
860-
}
861-
defer func() {
862-
// cleanupExecBundle MUST be called with the parent container locked.
863-
if !unlock && !c.batched {
864-
c.lock.Lock()
865-
unlock = true
866-
867-
if err := c.syncContainer(); err != nil {
868-
logrus.Errorf("Error syncing container %s state: %v", c.ID(), err)
869-
// Normally we'd want to continue here, get rid of the exec directory.
870-
// But the risk of proceeding into a function that can mutate state with a bad state is high.
871-
// Lesser of two evils is to bail and leak a directory.
872-
return
873-
}
874-
}
875-
if err := c.cleanupExecBundle(session.ID()); err != nil {
876-
logrus.Errorf("Container %s light exec session cleanup error: %v", c.ID(), err)
877-
}
878-
}()
879-
880-
pid, attachErrChan, err := c.ociRuntime.ExecContainer(c, session.ID(), opts, streams, nil)
881-
if err != nil {
882-
return -1, err
883-
}
884-
session.PID = pid
885-
session.PIDData = getPidData(pid)
886-
887-
if !c.batched {
888-
c.lock.Unlock()
889-
unlock = false
890-
}
891-
892-
select {
893-
case err = <-attachErrChan:
894-
if err != nil {
895-
return -1, fmt.Errorf("container %s light exec session with pid: %d error: %v", c.ID(), pid, err)
896-
}
897-
case <-time.After(timeout):
898-
if err := c.ociRuntime.ExecStopContainer(c, session.ID(), 0); err != nil {
899-
return -1, err
900-
}
901-
return -1, fmt.Errorf("%v of %s", define.ErrHealthCheckTimeout, c.HealthCheckConfig().Timeout.String())
902-
}
903-
904-
return c.readExecExitCode(session.ID())
824+
return c.execLightweight(config, streams, timeout)
905825
}
906826

907827
func (c *Container) Exec(config *ExecConfig, streams *define.AttachStreams, resize <-chan resize.TerminalSize) (int, error) {
@@ -1320,3 +1240,99 @@ func justWriteExecExitCode(c *Container, sessionID string, exitCode int, emitEve
13201240
// Finally, save our changes.
13211241
return c.save()
13221242
}
1243+
1244+
// execLightweight executes a command in a container without creating a persistent exec session.
1245+
// It is used by both ExecNoSession and healthCheckExec to avoid code duplication.
1246+
func (c *Container) execLightweight(config *ExecConfig, streams *define.AttachStreams, timeout time.Duration) (int, error) {
1247+
if err := c.verifyExecConfig(config); err != nil {
1248+
return -1, err
1249+
}
1250+
1251+
unlock := true
1252+
if !c.batched {
1253+
c.lock.Lock()
1254+
defer func() {
1255+
if unlock {
1256+
c.lock.Unlock()
1257+
}
1258+
}()
1259+
1260+
if err := c.syncContainer(); err != nil {
1261+
return -1, err
1262+
}
1263+
}
1264+
1265+
if !c.ensureState(define.ContainerStateRunning) {
1266+
return -1, fmt.Errorf("can only create exec sessions on running containers: %w", define.ErrCtrStateInvalid)
1267+
}
1268+
1269+
session, err := c.createExecSession(config)
1270+
if err != nil {
1271+
return -1, err
1272+
}
1273+
1274+
if c.state.ExecSessions == nil {
1275+
c.state.ExecSessions = make(map[string]*ExecSession)
1276+
}
1277+
c.state.ExecSessions[session.ID()] = session
1278+
defer delete(c.state.ExecSessions, session.ID())
1279+
1280+
opts, err := prepareForExec(c, session)
1281+
if err != nil {
1282+
return -1, err
1283+
}
1284+
defer func() {
1285+
if err := c.cleanupExecBundle(session.ID()); err != nil {
1286+
logrus.Errorf("Container %s light exec session cleanup error: %v", c.ID(), err)
1287+
}
1288+
}()
1289+
1290+
pid, attachErrChan, err := c.ociRuntime.ExecContainer(c, session.ID(), opts, streams, nil)
1291+
if err != nil {
1292+
// Check if the error is command not found before returning
1293+
if exitCode := define.ExitCode(err); exitCode == define.ExecErrorCodeNotFound {
1294+
return exitCode, err
1295+
}
1296+
return define.ExecErrorCodeGeneric, err
1297+
}
1298+
session.PID = pid
1299+
session.PIDData = getPidData(pid)
1300+
1301+
if !c.batched {
1302+
c.lock.Unlock()
1303+
unlock = false
1304+
}
1305+
1306+
// Handle timeout for health checks
1307+
if timeout > 0 {
1308+
select {
1309+
case err = <-attachErrChan:
1310+
if err != nil {
1311+
return -1, fmt.Errorf("container %s light exec session with pid: %d error: %v", c.ID(), pid, err)
1312+
}
1313+
case <-time.After(timeout):
1314+
if err := c.ociRuntime.ExecStopContainer(c, session.ID(), 0); err != nil {
1315+
return -1, err
1316+
}
1317+
return -1, fmt.Errorf("%v of %s", define.ErrHealthCheckTimeout, timeout.String())
1318+
}
1319+
} else {
1320+
// For no-session exec, wait for completion without timeout
1321+
err = <-attachErrChan
1322+
if err != nil && !errors.Is(err, define.ErrDetach) {
1323+
// Check if the error is command not found
1324+
if exitCode := define.ExitCode(err); exitCode == define.ExecErrorCodeNotFound {
1325+
return exitCode, err
1326+
}
1327+
return define.ExecErrorCodeGeneric, err
1328+
}
1329+
}
1330+
1331+
return c.readExecExitCode(session.ID())
1332+
}
1333+
1334+
// ExecNoSession executes a command in a container without creating a persistent exec session.
1335+
// It skips database operations and minimizes container locking for performance.
1336+
func (c *Container) ExecNoSession(config *ExecConfig, streams *define.AttachStreams, resize <-chan resize.TerminalSize) (int, error) {
1337+
return c.execLightweight(config, streams, 0)
1338+
}

pkg/domain/entities/engine_container.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type ContainerEngine interface { //nolint:interfacebloat
2626
ContainerCopyToArchive(ctx context.Context, nameOrID string, path string, writer io.Writer) (ContainerCopyFunc, error)
2727
ContainerCreate(ctx context.Context, s *specgen.SpecGenerator) (*ContainerCreateReport, error)
2828
ContainerExec(ctx context.Context, nameOrID string, options ExecOptions, streams define.AttachStreams) (int, error)
29+
ContainerExecNoSession(ctx context.Context, nameOrID string, options ExecOptions, streams define.AttachStreams) (int, error)
2930
ContainerExecDetached(ctx context.Context, nameOrID string, options ExecOptions) (string, error)
3031
ContainerExists(ctx context.Context, nameOrID string, options ContainerExistsOptions) (*BoolReport, error)
3132
ContainerExport(ctx context.Context, nameOrID string, options ContainerExportOptions) error

pkg/domain/infra/abi/containers.go

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -874,7 +874,7 @@ func (ic *ContainerEngine) ContainerAttach(ctx context.Context, nameOrID string,
874874
return nil
875875
}
876876

877-
func makeExecConfig(options entities.ExecOptions, rt *libpod.Runtime) (*libpod.ExecConfig, error) {
877+
func makeExecConfig(options entities.ExecOptions, rt *libpod.Runtime, noSession bool) (*libpod.ExecConfig, error) {
878878
execConfig := new(libpod.ExecConfig)
879879
execConfig.Command = options.Cmd
880880
execConfig.Terminal = options.Tty
@@ -887,18 +887,21 @@ func makeExecConfig(options entities.ExecOptions, rt *libpod.Runtime) (*libpod.E
887887
execConfig.PreserveFD = options.PreserveFD
888888
execConfig.AttachStdin = options.Interactive
889889

890-
// Make an exit command
891-
storageConfig := rt.StorageConfig()
892-
runtimeConfig, err := rt.GetConfig()
893-
if err != nil {
894-
return nil, fmt.Errorf("retrieving Libpod configuration to build exec exit command: %w", err)
895-
}
896-
// TODO: Add some ability to toggle syslog
897-
exitCommandArgs, err := specgenutil.CreateExitCommandArgs(storageConfig, runtimeConfig, logrus.IsLevelEnabled(logrus.DebugLevel), false, false, true)
898-
if err != nil {
899-
return nil, fmt.Errorf("constructing exit command for exec session: %w", err)
890+
// Only set up exit command for regular exec sessions, not no-session mode
891+
if !noSession {
892+
// Make an exit command
893+
storageConfig := rt.StorageConfig()
894+
runtimeConfig, err := rt.GetConfig()
895+
if err != nil {
896+
return nil, fmt.Errorf("retrieving Libpod configuration to build exec exit command: %w", err)
897+
}
898+
// TODO: Add some ability to toggle syslog
899+
exitCommandArgs, err := specgenutil.CreateExitCommandArgs(storageConfig, runtimeConfig, logrus.IsLevelEnabled(logrus.DebugLevel), false, false, true)
900+
if err != nil {
901+
return nil, fmt.Errorf("constructing exit command for exec session: %w", err)
902+
}
903+
execConfig.ExitCommand = exitCommandArgs
900904
}
901-
execConfig.ExitCommand = exitCommandArgs
902905

903906
return execConfig, nil
904907
}
@@ -948,7 +951,7 @@ func (ic *ContainerEngine) ContainerExec(ctx context.Context, nameOrID string, o
948951
util.ExecAddTERM(ctr.Env(), options.Envs)
949952
}
950953

951-
execConfig, err := makeExecConfig(options, ic.Libpod)
954+
execConfig, err := makeExecConfig(options, ic.Libpod, false)
952955
if err != nil {
953956
return ec, err
954957
}
@@ -957,6 +960,36 @@ func (ic *ContainerEngine) ContainerExec(ctx context.Context, nameOrID string, o
957960
return define.TranslateExecErrorToExitCode(ec, err), err
958961
}
959962

963+
func (ic *ContainerEngine) ContainerExecNoSession(ctx context.Context, nameOrID string, options entities.ExecOptions, streams define.AttachStreams) (int, error) {
964+
ec := define.ExecErrorCodeGeneric
965+
err := checkExecPreserveFDs(options)
966+
if err != nil {
967+
return ec, err
968+
}
969+
970+
containers, err := getContainers(ic.Libpod, getContainersOptions{latest: options.Latest, names: []string{nameOrID}})
971+
if err != nil {
972+
return ec, err
973+
}
974+
if len(containers) != 1 {
975+
return ec, fmt.Errorf("%w: expected to find exactly one container but got %d", define.ErrInternal, len(containers))
976+
}
977+
ctr := containers[0]
978+
979+
if options.Tty {
980+
util.ExecAddTERM(ctr.Env(), options.Envs)
981+
}
982+
983+
execConfig, err := makeExecConfig(options, ic.Libpod, true)
984+
if err != nil {
985+
return ec, err
986+
}
987+
988+
ec, err = ctr.ExecNoSession(execConfig, &streams, nil)
989+
// Translate exit codes for consistency with regular exec
990+
return define.TranslateExecErrorToExitCode(ec, err), err
991+
}
992+
960993
func (ic *ContainerEngine) ContainerExecDetached(ctx context.Context, nameOrID string, options entities.ExecOptions) (string, error) {
961994
err := checkExecPreserveFDs(options)
962995
if err != nil {
@@ -972,7 +1005,7 @@ func (ic *ContainerEngine) ContainerExecDetached(ctx context.Context, nameOrID s
9721005
}
9731006
ctr := containers[0]
9741007

975-
execConfig, err := makeExecConfig(options, ic.Libpod)
1008+
execConfig, err := makeExecConfig(options, ic.Libpod, false)
9761009
if err != nil {
9771010
return "", err
9781011
}

pkg/domain/infra/tunnel/containers.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,10 @@ func (ic *ContainerEngine) ContainerExec(ctx context.Context, nameOrID string, o
658658
return inspectOut.ExitCode, nil
659659
}
660660

661+
func (ic *ContainerEngine) ContainerExecNoSession(ctx context.Context, nameOrID string, options entities.ExecOptions, streams define.AttachStreams) (int, error) {
662+
return 0, errors.New("--no-session is not supported for the remote client")
663+
}
664+
661665
func (ic *ContainerEngine) ContainerExecDetached(ctx context.Context, nameOrID string, options entities.ExecOptions) (retSessionID string, retErr error) {
662666
createConfig := makeExecConfig(options)
663667

0 commit comments

Comments
 (0)