Documentation
¶
Overview ¶
Package externalsort sorts fixed-size records in lexicographic byte order using bounded chunks encrypted with AES-256-GCM in owner-only temporary storage. Start with NewFactory and Factory.Open, add records, iterate once with Store.ForEachSorted, and close every non-nil Store, even after a failed Open. Callback records are borrowed plaintext and must be copied before retention.
The package owns per-store storage and cleanup, not record encoding, key derivation, atomic output publication or crash recovery. Callers must protect process memory, isolate a trusted parent directory and coordinate shutdown. Context cancellation is cooperative; it cannot interrupt synchronous IO, entropy reads, sorting or a blocked callback. No goroutines, external service or cgo are introduced. Factory.Open refuses js and plan9 because rooted filesystem containment is unavailable there.
Example ¶
package main
import (
"bytes"
"context"
"fmt"
"os"
externalsort "github.com/faustbrian/go-external-sort"
)
func main() {
parent, err := os.MkdirTemp("", "external-sort-example-")
if err != nil {
panic(err)
}
defer func() {
if err := os.RemoveAll(parent); err != nil {
panic(err)
}
}()
factory, err := externalsort.NewFactory(externalsort.Config{
ParentDirectory: parent,
RecordBytes: 2,
ChunkRecords: 2,
MaximumRecords: 3,
})
if err != nil {
panic(err)
}
store, err := factory.Open(
context.Background(),
bytes.Repeat([]byte{1}, externalsort.AES256KeyBytes),
)
if store != nil {
defer func() {
if err := store.Close(); err != nil {
panic(err)
}
}()
}
if err != nil {
panic(err)
}
for _, record := range [][]byte{{3, 3}, {1, 1}, {2, 2}} {
if err := store.Add(context.Background(), record); err != nil {
panic(err)
}
}
if err := store.ForEachSorted(
context.Background(),
func(record []byte) error {
fmt.Printf("%x\n", record)
return nil
},
); err != nil {
panic(err)
}
}
Output: 0101 0202 0303
Index ¶
- Constants
- Variables
- type Config
- type Factory
- type Store
- func (store *Store) Add(ctx context.Context, record []byte) error
- func (store *Store) Close() error
- func (store *Store) ForEachSorted(ctx context.Context, yield func([]byte) error) (result error)
- func (Store) GoString() string
- func (Store) LogValue() slog.Value
- func (Store) MarshalJSON() ([]byte, error)
- func (Store) String() string
Examples ¶
Constants ¶
const ( // AES256KeyBytes is the exact key size accepted by Factory.Open. AES256KeyBytes = 32 // MaximumMergeFiles is the hard upper bound for one merge fan-in. MaximumMergeFiles = 64 // MaximumRecordBytes bounds one fixed-size record. MaximumRecordBytes = 1 << 20 // MaximumChunkRecords bounds the number of records held in memory. MaximumChunkRecords = 1_000_000 // MaximumChunkBytes bounds the contiguous in-memory sort buffer. MaximumChunkBytes = 256 << 20 )
Variables ¶
var ( // ErrInvalidConfiguration reports invalid bounds, nil required inputs or an // unconstructed Factory/Store. ErrInvalidConfiguration = errors.New( "external sort configuration is invalid", ) // ErrUnsafeParent reports a missing, replaced or insufficiently restricted // parent directory, or a platform without supported rooted containment. ErrUnsafeParent = errors.New( "external sort parent is not an owner-only directory", ) // ErrInvalidKey reports a key whose length is not AES256KeyBytes. ErrInvalidKey = errors.New("external sort key is invalid") // ErrInvalidRecord reports a record whose length differs from Config.RecordBytes. ErrInvalidRecord = errors.New("external sort record is invalid") // ErrRecordLimit reports an Add beyond Config.MaximumRecords. ErrRecordLimit = errors.New("external sort record limit reached") // ErrConcurrentUse reports overlap with an active store lifecycle operation. ErrConcurrentUse = errors.New("external sort store is already in use") // ErrClosed reports input or iteration attempted after closing begins, // including cleanup-only handles returned by a failed Open. ErrClosed = errors.New("external sort store is closed") // ErrFinalized reports input or iteration attempted after finalization. ErrFinalized = errors.New("external sort store is finalized") // ErrEntropy reports failed randomness or exhausted nonce uniqueness state. ErrEntropy = errors.New("external sort entropy failed") // ErrStorage reports owned IO or cleanup failure without exposing paths // or wrapping underlying filesystem errors. ErrStorage = errors.New("external sort storage failed") // ErrCorrupt reports invalid encrypted framing or authentication, // including unexpected truncation or trailing bytes. ErrCorrupt = errors.New("external sort chunk is corrupt") )
Functions ¶
This section is empty.
Types ¶
type Config ¶
type Config struct {
// ParentDirectory is an existing absolute directory with no group/other
// permission bits. Its final component must not be a symlink; ancestor
// links are resolved at construction. The caller must exclude hostile writers.
ParentDirectory string
// RecordBytes is the exact positive record width, at most MaximumRecordBytes.
RecordBytes int
// ChunkRecords is the positive in-memory record capacity, at most
// MaximumChunkRecords. RecordBytes * ChunkRecords must fit MaximumChunkBytes.
ChunkRecords int
// MaximumRecords is the total input ceiling, at least ChunkRecords.
// The rounded-up chunk count must not exceed MaximumMergeFiles.
MaximumRecords int
}
Config declares per-store storage and buffer bounds. There are no defaults; the zero value is invalid. These bounds do not limit process-wide memory, caller copies, cryptographic scratch or the number of concurrent stores.
type Factory ¶
type Factory struct {
// contains filtered or unexported fields
}
Factory holds a validated reusable policy and parent-directory identity. Obtain it from NewFactory; its zero value cannot Open a store. It is safe for concurrent Open calls, which create independent stores. The factory owns no open directory handle or Close obligation; every store owns its own resources.
func NewFactory ¶
NewFactory copies config after validating its bounds and existing absolute parent directory. The final parent component must not be a symlink and its group/other permission bits must be clear. Existing ancestor links are resolved once; Open revalidates the resolved directory's identity and permissions. Invalid bounds return nil and ErrInvalidConfiguration; filesystem/parent validation failures return nil and ErrUnsafeParent without revealing paths. No OS-owner or hostile-writer isolation check is performed.
func (Factory) GoString ¶
GoString prevents configuration and temporary paths from entering debug representations.
func (Factory) MarshalJSON ¶
MarshalJSON prevents configuration and temporary paths from entering JSON.
func (*Factory) Open ¶
Open creates an independent store with a rooted parent handle, owner-only work directory and bounded plaintext buffer. ctx must be non-nil and key must have AES256KeyBytes bytes; nil/zero factories return ErrInvalidConfiguration. Key bytes are consumed during cipher construction, not retained as the input slice. Do not mutate key during Open. The caller owns its lifetime; cipher schedules are not securely erased. Use an independently derived key per dataset across processes/restarts.
Open checks cancellation cooperatively and returns context errors directly. Parent validation, entropy and storage failures use ErrUnsafeParent, ErrEntropy and ErrStorage. On js/plan9, rooted storage returns ErrUnsafeParent. These errors do not wrap sensitive underlying IO values.
The caller must Close every non-nil Store, including one returned with an error: failed construction cleanup can leave an owned cleanup-only handle. Such a handle rejects Add/ForEachSorted with ErrClosed and retries removal through Close. A nil Store transfers no resource ownership.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store owns encrypted temporary chunks for one sort; obtain it from Factory.Open. Its zero value is invalid. Copies of Store share the same lifecycle and resources; they are not independent sorts. The caller owns Close.
One lifecycle operation is admitted at a time. While open, overlap/reentry returns ErrConcurrentUse; input/iteration after closing begins returns ErrClosed. Close also rejects overlap and does not interrupt active work. No store lock is held across IO or callbacks and no goroutine starts. Callback records are borrowed plaintext, cleared when the callback returns.
func (*Store) Add ¶
Add copies one exact Config.RecordBytes record into the bounded chunk. The caller may reuse the slice after return, but must not mutate it during Add. A full chunk is sorted and encrypted to disk before Add returns. Nil store or context returns ErrInvalidConfiguration; wrong width returns ErrInvalidRecord, and exceeding MaximumRecords returns ErrRecordLimit. Lifecycle errors are ErrConcurrentUse, ErrClosed and ErrFinalized.
A failed Add does not accept the new record and preserves previously accepted records. It may be retried after cancellation, entropy or storage failure unless partial-file cleanup fails, which permits only Close. Spill retries consume new nonce counters rather than reusing a failed attempt's nonce. Context errors pass through; owned IO failures use payload-free sentinels.
func (*Store) Close ¶
Close clears buffered plaintext, removes the work directory and releases the rooted handle. A nil Store succeeds; an unconstructed zero Store returns ErrInvalidConfiguration. Close rejects active operations with ErrConcurrentUse instead of waiting or interrupting them. It has no context and can block in IO.
Once closing begins, Add/ForEachSorted return ErrClosed. A removal failure returns ErrStorage and leaves only Close retryable. After successful removal, state is finalized even if rooted-handle closure reports ErrStorage; later Close calls return nil without retrying that handle. Otherwise repeated Close is idempotent. Dropping cipher references is not secure key-memory erasure, and process death can bypass cleanup entirely.
func (*Store) ForEachSorted ¶
ForEachSorted finalizes input and calls yield synchronously for every record in ascending full-record byte order, preserving duplicates. There is no custom comparator or key-only stable-sort contract. Empty input succeeds without a callback. A nil store, context or yield returns ErrInvalidConfiguration.
Each callback slice is cleared on return, including panic; copy before retaining it. Callback errors and context errors pass through unchanged and callback panics propagate after plaintext is cleared and reader closure is attempted. A blocked callback can delay cancellation; reentrant lifecycle calls on this store are rejected, not awaited. Cancellation after the final checkpoint need not turn otherwise successful iteration into an error.
Output is not atomic: corruption, storage, cancellation or callback failure can follow records already yielded. Reader-close failure can return ErrStorage even after complete output; it does not override an earlier iteration error. Once pending input is spilled, the store remains finalized even on failure, including empty input. Further Add/ForEachSorted calls return ErrFinalized. A pre-finalization failure is retryable only if cleanup has not sealed the store. The caller must Close on every path; iteration does not delete chunks.
func (Store) GoString ¶
GoString prevents records, cipher state, and temporary paths from entering debug representations.
func (Store) LogValue ¶
LogValue prevents records, cipher state, and temporary paths from entering slog.
func (Store) MarshalJSON ¶
MarshalJSON prevents records, cipher state, and temporary paths from entering JSON.