Class: LittleGhost::Artifacts::WorkspaceStore

Inherits:
Object
  • Object
show all
Defined in:
lib/little_ghost/artifacts/workspace_store.rb

Overview

Writes immutable artifacts beneath one named Workspace path. The store applies per-artifact, total-byte, and count limits before exposing a file. It does not remove files; the Workspace provider owns their lifetime.

Defined Under Namespace

Classes: ContextControl

Constant Summary collapse

PLATFORM_OPEN_FLAGS =
case RUBY_PLATFORM
when /darwin/
  {close_on_exec: 0x0100_0000}
when /linux/
  {close_on_exec: 0x0008_0000}
end&.freeze
OPENAT =

:nodoc:

if PLATFORM_OPEN_FLAGS # :nodoc:
  Fiddle::Function.new(
    Fiddle::Handle::DEFAULT["openat"],
    [Fiddle::TYPE_INT, Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT, Fiddle::TYPE_VARIADIC],
    Fiddle::TYPE_INT
  )
end
UNLINKAT =

:nodoc:

if PLATFORM_OPEN_FLAGS # :nodoc:
  Fiddle::Function.new(
    Fiddle::Handle::DEFAULT["unlinkat"],
    [Fiddle::TYPE_INT, Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT],
    Fiddle::TYPE_INT
  )
end
DEFAULT_MAX_ARTIFACT_BYTES =

Default maximum size of one artifact: 32 MiB.

32 * 1024 * 1024
DEFAULT_MAX_TOTAL_BYTES =

Default cumulative store budget: 64 MiB.

64 * 1024 * 1024
DEFAULT_MAX_ARTIFACTS =

Default maximum number of unique artifacts.

100

Instance Method Summary collapse

Constructor Details

#initialize(workspace:, path: :artifacts, max_artifact_bytes: DEFAULT_MAX_ARTIFACT_BYTES, max_total_bytes: DEFAULT_MAX_TOTAL_BYTES, max_artifacts: DEFAULT_MAX_ARTIFACTS) ⇒ WorkspaceStore

Uses the already-open workspace and a configured named path.

Raises:



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/little_ghost/artifacts/workspace_store.rb', line 55

def initialize(
  workspace:,
  path: :artifacts,
  max_artifact_bytes: DEFAULT_MAX_ARTIFACT_BYTES,
  max_total_bytes: DEFAULT_MAX_TOTAL_BYTES,
  max_artifacts: DEFAULT_MAX_ARTIFACTS
)
  raise ToolError, "Artifact workspace path is unavailable" unless workspace

  @workspace = workspace
  @path = path.to_sym
  @max_artifact_bytes = positive_integer(max_artifact_bytes, :max_artifact_bytes)
  @max_total_bytes = positive_integer(max_total_bytes, :max_total_bytes)
  @max_artifacts = positive_integer(max_artifacts, :max_artifacts)
  @mutex = Mutex.new
  @artifacts = {}
  @total_bytes = 0
  @namespace = SecureRandom.hex(8).freeze
  @root, @root_identity = validate_root!
end

Instance Method Details

#sizeObject

Number of unique artifacts in this store.



119
# File 'lib/little_ghost/artifacts/workspace_store.rb', line 119

def size = @mutex.synchronize { @artifacts.length }

#total_bytesObject

Number of bytes reserved by unique artifacts in this store.



116
# File 'lib/little_ghost/artifacts/workspace_store.rb', line 116

def total_bytes = @mutex.synchronize { @total_bytes }

#write(data:, name: nil, media_type: nil, metadata: {}, context: nil) ⇒ Object

Materializes data once for an identical name, media type, and byte sequence. Concurrent calls share one descriptor and consume one budget.



78
79
80
# File 'lib/little_ghost/artifacts/workspace_store.rb', line 78

def write(data:, name: nil, media_type: nil, metadata: {}, context: nil)
  write_batch([{data:, name:, media_type:, metadata:}], context:).fetch(0)
end

#write_batch(entries, context: nil) ⇒ Object

Atomically materializes an Array of write keyword Hashes. If any new artifact fails, every file and budget reservation created by this call is rolled back; previously cached artifacts remain unchanged.

Raises:

  • (ArgumentError)


85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/little_ghost/artifacts/workspace_store.rb', line 85

def write_batch(entries, context: nil)
  raise ArgumentError, "artifact batch must be an array" unless entries.is_a?(Array)

  context&.check!
  control = context_control(context)
  prepared = entries.map { |entry| prepare_entry(entry) }
  @mutex.synchronize do
    control&.check!
    created = []
    begin
      prepared.map do |entry|
        control&.check!
        digest = entry.fetch(:digest)
        next @artifacts.fetch(digest) if @artifacts.key?(digest)

        reserve!(entry.fetch(:data).bytesize)
        record = entry.merge(materialized: false)
        created << record
        artifact = materialize(**entry, control:)
        @artifacts[digest] = artifact
        record[:materialized] = true
        artifact
      end.freeze
    rescue => error
      rollback_batch(created, error:)
      raise
    end
  end
end