[Horde] Horde FileSystem backend on SMB/NAS – artifact retrieval timeouts and possible MemoryMappedFileCache contention

We are investigating increasingly frequent artifact retrieval issues with our Horde installation and would appreciate Epic’s input.

Over the last few weeks we have seen:

  • Artifact retrieval timeouts becoming more frequent
  • Occasional retrieved data whose size differs from the expected artifact/manifest data
  • Very slow artifact retrieval
  • Horde UI operations that need to retrieve storage-backed data becoming slow at the same time
  • No obvious CPU or memory saturation on the Horde Server
  • SMB throughput that does not appear saturated
  • Occasional SMB/file access latency spikes

We traced the artifact read path down to:

BatchBlobReader -> BundleHandle -> FileStorageBackend -> FileObjectStore -> MemoryMappedFileCache

One thing caught our attention.

FileObjectStore.ReadAsync() is effectively synchronous and eventually calls MemoryMappedFileCache.Read(). MemoryMappedFileCache.Read() holds a global lock while FindOrAddMappedFile() runs. On a cache miss, FindOrAddMappedFile() performs filesystem operations such as checking FileInfo.Exists, FileInfo.Length, possible cache eviction, and creation of a MemoryMappedFile / view accessor.

Because our files are on an SMB UNC path, it appears that remote filesystem operations can occur while that global lock is held.

We also confirmed that FileObjectStoreFactory is registered as a singleton and owns a single MemoryMappedFileCache shared by the FileObjectStore instances it creates.

The default MemoryMappedFileCache limits are:

1 GB maximum mapped size

128 mapped files

Our current hypothesis is that occasional SMB latency could be amplified by this shared lock. If opening or mapping an uncached blob takes hundreds of milliseconds, other concurrent artifact reads using the same cache could wait behind it, temporarily serializing reads.

This may also explain why UI operations that need storage-backed data slow down at the same time as artifact retrieval.

We have not confirmed this as the root cause yet. Our next planned test is to instrument MemoryMappedFileCache.Read() to separately measure:

  • time waiting to acquire the lock
  • time spent in FindOrAddMappedFile()
  1. Is using a FileSystem backend over SMB with roughly 20 ms RTT an expected/recommended Horde topology?
  2. Does Epic use remote SMB/NAS storage for Horde artifacts internally, or is low-latency/object storage generally expected?
  3. Could MemoryMappedFileCache’s shared lock become a bottleneck with higher-latency filesystem storage?
  4. Are the default 1 GB / 128 mapped file limits expected to work well for large artifact workloads?
  5. Would increasing those limits be a reasonable diagnostic test?

We are mainly trying to determine whether this is a NAS/network issue, a storage topology issue, or SMB latency being amplified by Horde’s FileSystem backend implementation.

[Attachment Removed]

Hello, please find my answers to your questions below, and let me know if you have any follow-ups.

On the artifact retrieval timeouts, this recent change might be worth cherry-picking and testing, as it bumps the default timeout for the HttpClient for storage requests to 300s.

We found that the default 100s timeout could cut short the retry policy on storage requests, causing unnecessary job step failures on transient network errors that were later recovered.

https://github.com/EpicGames/UnrealEngine/commit/0df89cad38679e9d3926c50ef23f753575314970

For the file size mismatches, could you please share the log error message that you see when this occurs?

You can also try to reproduce the artifact download with Horde CMD tool. Adding -VerifyOutput will make the tool throw an error on a mismatch.

horde artifact download ... -VerifyOutput
horde archive extract ... -VerifyOutput

1. Is using a FileSystem backend over SMB with roughly 20 ms RTT an expected/recommended Horde topology?

Generally in the past we have recommended against using a filesystem backend for production use-cases.

So, unfortunately, I don’t have any guidance on latency numbers that could tell you how well this will perform for you.

The main reason is that storage backends come in two flavors, those that support signed URLs, and those that do not.

Backends like S3, or Azure/GCP object storage, support the signed URLs, and when the agents request an artifact from the Horde server they actually get a signed URL, which allows the agents to download the artifacts directly from the object store.

This works really well as these object storage solutions are designed to scale horizontally and the agents can benefit from the data chunking for parallelism.

If you’re using the local filesystem then Horde cannot redirect the agents to pull the artifact directly from the storage and must itself serve the file, which is much slower.

If you’re deploying a Horde server on-prem, we have in the past seen licensee success with the deployment of S3-compatible object storage, such as MinIO, but this may be out of scope.

2. Does Epic use remote SMB/NAS storage for Horde artifacts internally, or is low-latency/object storage generally expected?

No, internally at Epic, we use AWS S3 Object Store.

3. Could MemoryMappedFileCache’s shared lock become a bottleneck with higher-latency filesystem storage?

It looks possible, and adding the telemetry seems like a good way to check this.

I couldn’t find any internal discussion about this, but given we’re on S3, I wouldn’t expect us to have run into this much (only via customer reports).

4. Are the default 1 GB / 128 mapped file limits expected to work well for large artifact workloads?

5. Would increasing those limits be a reasonable diagnostic test?

Again, unfortunately, we don’t have any specific guidance on these settings, and they’ve remained the same since the initial implementation of the filesystem backend.

I would expect them to be suitable, but tuning them could be a worthwhile test to run.

[Attachment Removed]

Hello, again! I’ve added some information below based on our chat outside of this discussion.

Please let me know if you have any further questions.

Horde & Azure Blob Storage

I was able to find some more information on the issue with Azure Blob Storage and Horde 5.5.

It seems as though Azure was added as an option in the StorageBackendType enum, but wasn’t handled correctly when set.

It was actually in the 5.5 release that this was addressed, so this wouldn’t work pre-5.5, but on 5.5 or later, you should be good to go.

The change containing the fix was CL 36756615, which is unfortunately a large merge commit, but I believe you’re already ahead of this.

https://github.com/EpicGames/UnrealEngine/commit/2af764e351f1acd86e49b4c322eff4f84789455e\#diff\-8d1a08404214e013f80569a18b4514f9e1f9bfae86aeb98711e54a2362fd0134

As for other outstanding issues, I came across this GitHub PR that may be worth looking into, though we have not validated this internally.

https://github.com/EpicGames/UnrealEngine/pull/14868

There is also this much larger GitHub PR for wider Azure support, but this may not be required if using on-prem agents.

https://github.com/EpicGames/UnrealEngine/pull/13464

Horde & Azure Blob Storage

Horde has the following artifact expiration settings, which are set per artifact type:

  • KeepCount: retains the most recent N artifacts of a particular type in each stream.
  • KeepDays: retains each artifact for N days.

https://github.com/EpicGames/UnrealEngine/blob/5\.6\.1\-release/Engine/Source/Programs/Horde/Docs/Config/Artifacts.md\#expiration

On the Storage side, garbage collection is toggled with the “enableGc” setting on the StorageConfig object, which defaults to on.

You can tweak the GC frequency and retention delays using the storage namespaces config (see NamespaceConfig).

https://github.com/EpicGames/UnrealEngine/blob/5\.6\.1\-release/Engine/Source/Programs/Horde/Docs/Config/Storage.md

https://github.com/EpicGames/UnrealEngine/blob/5\.6\.1\-release/Engine/Source/Programs/Horde/Docs/Config/Schema/Globals.md\#storageconfig

https://github.com/EpicGames/UnrealEngine/blob/5\.6\.1\-release/Engine/Source/Programs/Horde/Docs/Config/Schema/Globals.md\#namespaceconfig

The default settings can be found here:

Heya Ieuan!!

Thanks for all the information and the different leads!

We’ve now switched our artifact storage from FileSystem to S3, and so far it’s been working flawlessly.

We did initially try Azure Blob Storage, though, and that ended in a rather spectacular failure. :slight_smile:

We were consistently getting the following error:

Status: BadRequest Reason: An HTTP header that’s mandatory for this request is not specified.

Rather than digging into Horde’s default Azure Blob request and trying to figure out which required header was missing, we decided to move forward with S3 instead.

So far, so good! Thanks again for pointing us in the right direction.

[Attachment Removed]

Hello! That’s great news that you’re set up and running! I’ll close out this case, but let me know if you have any further questions.

If you’re happy with the S3 setup, then I would be inclined to leave the Azure issue alone, as it’s likely a setup-specific config issue.

[Attachment Removed]