{"record":{"id":"7c5a44afd4fc38ec","repo":"puppetlabs/puppet","slug":"timeout-waiting-for-exclusive-lock-on-path","errorCode":null,"errorMessage":"Timeout waiting for exclusive lock on %{path}","messagePattern":"Timeout waiting for exclusive lock on %(.+?)","errorType":"exception","errorClass":"Timeout::Error","httpStatus":null,"severity":"error","filePath":"lib/puppet/file_system/file_impl.rb","lineNumber":68,"sourceCode":"  end\n\n  def exclusive_open(path, mode, options = 'r', timeout = 300, &block)\n    wait = 0.001 + (Kernel.rand / 1000)\n    written = false\n    until written\n      ::File.open(path, options, mode) do |rf|\n        if rf.flock(::File::LOCK_EX | ::File::LOCK_NB)\n          Puppet.debug { _(\"Locked '%{path}'\") % { path: path } }\n          yield rf\n          written = true\n          Puppet.debug { _(\"Unlocked '%{path}'\") % { path: path } }\n        else\n          Puppet.debug { \"Failed to lock '%s' retrying in %.2f milliseconds\" % [path, wait * 1000] }\n          sleep wait\n          timeout -= wait\n          wait *= 2\n          if timeout < 0\n            raise Timeout::Error, _(\"Timeout waiting for exclusive lock on %{path}\") % { path: path }\n          end\n        end\n      end\n    end\n  end\n\n  def each_line(path, &block)\n    ::File.open(path) do |f|\n      f.each_line do |line|\n        yield line\n      end\n    end\n  end\n\n  def read(path, opts = {})\n    path.read(**opts)\n  end\n","sourceCodeStart":50,"sourceCodeEnd":86,"githubUrl":"https://github.com/puppetlabs/puppet/blob/e227c27540975c25aa22d533a52424a9d2fc886a/lib/puppet/file_system/file_impl.rb#L50-L86","documentation":"Puppet's atomic write helper (FileImpl#replace_file) opens the target and tries flock LOCK_EX|LOCK_NB; on contention it sleeps with exponential backoff while a timeout budget lasts, then raises Timeout::Error. Another process is holding an exclusive lock on the same file, typically concurrent Puppet runs or a stuck process sharing the same vardir/file.","triggerScenarios":"Two puppet agent processes sharing one vardir (containers with a shared volume, or a stray agent plus a cron run); a hung ruby/puppetserver process never releasing flock; Puppet::FileSystem.replace_file called concurrently by custom face/tool code on the same path.","commonSituations":"Overlapping puppet agent schedules (runinterval shorter than run time plus manual `puppet agent --test`); Docker images running agents against a mounted vardir; orphaned processes after a crashed run.","solutions":["Find the lock holder: `lsof /path/to/file` or `fuser -v`, then stop that process cleanly","Prevent overlap: ensure one agent per vardir (unique ssldir/vardir per container), fix runinterval/cron so runs cannot collide","If the holder is dead but state is stale, verify with lsof that no fd remains, then retry the operation","In custom code using replace_file, catch Timeout::Error and retry with backoff, or serialize access yourself"],"exampleFix":"# before\nPuppet::FileSystem.replace_file(path, 0o644) { |f| f.write(data) } # may raise Timeout::Error\n\n# after\nattempts = 0\nbegin\n  attempts += 1\n  Puppet::FileSystem.replace_file(path, 0o644) { |f| f.write(data) }\nrescue Timeout::Error\n  raise if attempts >= 3\n  sleep(2**attempts)\n  retry\nend","handlingStrategy":"retry","validationCode":"# Before writing, confirm no other process holds the file\ndef locked_by?(path)\n  !system(\"lsof -- #{Shellwords.escape(path)} >/dev/null 2>&1\")\nend","typeGuard":null,"tryCatchPattern":"attempts = 0\nbegin\n  Puppet::FileSystem.replace_file(path, mode) { |f| f.write(data) }\nrescue Timeout::Error\n  attempts += 1\n  raise if attempts > 3\n  sleep(2**attempts)\n  retry\nend","preventionTips":["One agent per vardir: give containers/pod their own ssldir and vardir","Space runinterval and cron jobs so runs cannot overlap","Reap orphaned puppet/ruby processes after crashes before the next run"],"tags":["puppet","file-system","file-lock","timeout","concurrency"],"backgroundTag":"file-lock-contention","analyzedSha":"e227c27540975c25aa22d533a52424a9d2fc886a","analyzedAt":"2026-08-21T20:49:46.650Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}