mirror of
https://github.com/nestriness/cdc-file-transfer.git
synced 2026-01-30 12:25:35 +02:00
This change introduces dynamic manifest updates to asset streaming. Asset streaming describes the directory to be streamed in a manifest, which is a proto definition of all content metadata. This information is sufficient to answer `stat` and `readdir` calls in the FUSE layer without additional round-trips to the workstation. When a directory is streamed for the first time, the corresponding manifest is created in two steps: 1. The directory is traversed recursively and the inode information of all contained files and directories is written to the manifest. 2. The content of all identified files is processed to generate each file's chunk list. This list is part of the definition of a file in the manifest. * The chunk boundaries are identified using our implementation of the FastCDC algorithm. * The hash of each chunk is calculated using the BLAKE3 hash function. * The length and hash of each chunk is appended to the file's chunk list. Prior to this change, when the user mounted a workstation directory on a client, the asset streaming server pushed an intermediate manifest to the gamelet as soon as step 1 was completed. At this point, the FUSE client started serving the virtual file system and was ready to answer `stat` and `readdir` calls. In case the FUSE client received any call that required file contents, such as `read`, it would block the caller until the server completed step 2 above and pushed the final manifest to the client. This works well for large directories (> 100GB) with a reasonable number of files (< 100k). But when dealing with millions of tiny files, creating the full manifest can take several minutes. With this change, we introduce dynamic manifest updates. When the FUSE layer receives an `open` or `readdir` request for a file or directory that is incomplete, it sends an RPC to the workstation about what information is missing from the manifest. The workstation identifies the corresponding file chunker or directory scanner tasks and moves them to the front of the queue. As soon as the task is completed, the workstation pushes an updated intermediate manifest to the client which now includes the information to serve the FUSE request. The queued FUSE request is resumed and returns the result to the caller. While this does not reduce the required time to build the final manifest, it splits up the work into smaller tasks. This allows us to interrupt the current work and prioritize those tasks which are required to handle an incoming request from the client. While this still takes a round-trip to the workstation plus the processing time for the task, an updated manifest is received within a few seconds, which is much better than blocking for several minutes. This latency is only visible when serving data while the manifest is still being created. The situation improves as the manifest creation on the workstation progresses. As soon as the final manifest is pushed, all metadata can be served directly without having to wait for pending tasks.
268 lines
11 KiB
C++
268 lines
11 KiB
C++
/*
|
|
* Copyright 2022 Google LLC
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#ifndef ASSET_STREAM_MANAGER_MULTI_SESSION_H_
|
|
#define ASSET_STREAM_MANAGER_MULTI_SESSION_H_
|
|
|
|
#include <memory>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <unordered_map>
|
|
|
|
#include "absl/status/status.h"
|
|
#include "absl/status/statusor.h"
|
|
#include "asset_stream_manager/asset_stream_server.h"
|
|
#include "asset_stream_manager/metrics_recorder.h"
|
|
#include "asset_stream_manager/session_config.h"
|
|
#include "common/stopwatch.h"
|
|
#include "data_store/data_store_writer.h"
|
|
#include "manifest/file_chunk_map.h"
|
|
#include "manifest/manifest_updater.h"
|
|
|
|
namespace cdc_ft {
|
|
|
|
class ProcessFactory;
|
|
class Session;
|
|
using ManifestUpdatedCb = std::function<void()>;
|
|
|
|
// Updates the manifest and runs a file watcher in a background thread.
|
|
class MultiSessionRunner {
|
|
public:
|
|
// |src_dir| is the source directory on the workstation to stream.
|
|
// |data_store| can be passed for tests to override the default store used.
|
|
// |process_factory| abstracts process creation.
|
|
// |enable_stats| shows whether statistics should be derived.
|
|
// |wait_duration| is the waiting time for changes in the streamed directory.
|
|
// |num_updater_threads| is the thread count for the manifest updater.
|
|
// |manifest_updated_cb| is the callback executed when a new manifest is set.
|
|
MultiSessionRunner(
|
|
std::string src_dir, DataStoreWriter* data_store,
|
|
ProcessFactory* process_factory, bool enable_stats,
|
|
absl::Duration wait_duration, uint32_t num_updater_threads,
|
|
MultiSessionMetricsRecorder const* metrics_recorder,
|
|
ManifestUpdatedCb manifest_updated_cb = ManifestUpdatedCb());
|
|
|
|
~MultiSessionRunner() = default;
|
|
|
|
// Starts |server_| of |type| on |port|.
|
|
absl::Status Initialize(
|
|
int port, AssetStreamServerType type,
|
|
ContentSentHandler content_sent = ContentSentHandler());
|
|
|
|
// Stops updating the manifest and |server_|.
|
|
absl::Status Shutdown() ABSL_LOCKS_EXCLUDED(mutex_);
|
|
|
|
// Waits until a manifest is ready and the gamelet |instance_id| has
|
|
// acknowledged the reception of the currently set manifest id. |fuse_timeout|
|
|
// is the timeout for waiting for the FUSE manifest ack. The time required to
|
|
// generate the manifest is not part of this timeout as this could take a
|
|
// longer time for a directory with many files.
|
|
absl::Status WaitForManifestAck(const std::string& instance_id,
|
|
absl::Duration fuse_timeout);
|
|
|
|
absl::Status Status() ABSL_LOCKS_EXCLUDED(mutex_);
|
|
|
|
// Returns the current manifest id used by |server_|.
|
|
ContentIdProto ManifestId() const;
|
|
|
|
private:
|
|
// Updates manifest if the content of the watched directory changes and
|
|
// distributes it to subscribed gamelets.
|
|
void Run();
|
|
|
|
// Record MultiSessionStart event.
|
|
void RecordMultiSessionStart(const ManifestUpdater& manifest_updater);
|
|
|
|
// Record ManifestUpdate event.
|
|
void RecordManifestUpdate(const ManifestUpdater& manifest_updater,
|
|
absl::Duration duration,
|
|
metrics::UpdateTrigger trigger,
|
|
absl::Status status);
|
|
|
|
void SetStatus(absl::Status status) ABSL_LOCKS_EXCLUDED(mutex_);
|
|
|
|
// Files changed callback called from FileWatcherWin.
|
|
void OnFilesChanged() ABSL_LOCKS_EXCLUDED(mutex_);
|
|
|
|
// Directory recreated callback called from FileWatcherWin.
|
|
void OnDirRecreated() ABSL_LOCKS_EXCLUDED(mutex_);
|
|
|
|
// Called during manifest update when the intermediate manifest or the final
|
|
// manifest is available. Pushes the manifest to connected FUSEs.
|
|
void SetManifest(const ContentIdProto& manifest_id);
|
|
|
|
const std::string src_dir_;
|
|
DataStoreWriter* const data_store_;
|
|
ProcessFactory* const process_factory_;
|
|
FileChunkMap file_chunks_;
|
|
const absl::Duration wait_duration_;
|
|
const uint32_t num_updater_threads_;
|
|
const ManifestUpdatedCb manifest_updated_cb_;
|
|
std::unique_ptr<AssetStreamServer> server_;
|
|
std::unique_ptr<ManifestUpdater> manifest_updater_;
|
|
|
|
// Modifications (shutdown, file changes).
|
|
absl::Mutex mutex_;
|
|
bool shutdown_ ABSL_GUARDED_BY(mutex_) = false;
|
|
bool files_changed_ ABSL_GUARDED_BY(mutex_) = false;
|
|
bool dir_recreated_ ABSL_GUARDED_BY(mutex_) = false;
|
|
bool manifest_set_ ABSL_GUARDED_BY(mutex_) = false;
|
|
Stopwatch files_changed_timer_ ABSL_GUARDED_BY(mutex_);
|
|
absl::Status status_ ABSL_GUARDED_BY(mutex_);
|
|
|
|
// Background thread that watches files and updates the manifest.
|
|
std::unique_ptr<std::thread> thread_;
|
|
|
|
MultiSessionMetricsRecorder const* metrics_recorder_;
|
|
};
|
|
|
|
// Manages an asset streaming session from a fixed directory on the workstation
|
|
// to an arbitrary number of gamelets.
|
|
class MultiSession {
|
|
public:
|
|
// Maximum length of cache path. We must be able to write content hashes into
|
|
// this path:
|
|
// <cache path>\01234567890123456789<null terminator> = 260 characters.
|
|
static constexpr size_t kDefaultMaxCachePathLen =
|
|
260 - 1 - ContentId::kHashSize * 2 - 1;
|
|
|
|
// Length of the hash appended to the cache directory, exposed for testing.
|
|
static constexpr size_t kDirHashLen = 8;
|
|
|
|
// |src_dir| is the source directory on the workstation to stream.
|
|
// |cfg| contains generic configuration parameters for each session.
|
|
// |process_factory| abstracts process creation.
|
|
// |data_store| can be passed for tests to override the default store used.
|
|
// By default, the class uses a DiskDataStore that writes to
|
|
// %APPDATA%\GGP\asset_streaming|<dir_derived_from_src_dir> on Windows.
|
|
MultiSession(std::string src_dir, SessionConfig cfg,
|
|
ProcessFactory* process_factory,
|
|
MultiSessionMetricsRecorder const* metrics_recorder,
|
|
std::unique_ptr<DataStoreWriter> data_store = nullptr);
|
|
~MultiSession();
|
|
|
|
// Initializes the data store if not overridden in the constructor and starts
|
|
// a background thread for updating the manifest and watching file changes.
|
|
// Does not wait for the initial manifest update to finish. Use IsRunning()
|
|
// to determine whether it is finished.
|
|
// Not thread-safe.
|
|
absl::Status Initialize();
|
|
|
|
// Stops all sessions and shuts down the server.
|
|
// Not thread-safe.
|
|
absl::Status Shutdown() ABSL_LOCKS_EXCLUDED(shutdownMu_);
|
|
|
|
// Returns the |src_dir| streaming directory passed to the constructor.
|
|
const std::string& src_dir() const { return src_dir_; }
|
|
|
|
// Returns the status of the background thread.
|
|
// Not thread-safe.
|
|
absl::Status Status();
|
|
|
|
// Starts a new streaming session to the instance with given |instance_id| and
|
|
// waits until the FUSE has received the initial manifest id.
|
|
// Returns an error if a session for that instance already exists.
|
|
// |instance_id| is the instance id of the target remote instance.
|
|
// |project_id| is id of the project that contains the instance.
|
|
// |organization_id| is id of the organization that contains the instance.
|
|
// |instance_ip| is the IP address of the instance.
|
|
// |instance_port| is the SSH port for connecting to the remote instance.
|
|
// Thread-safe.
|
|
absl::Status StartSession(const std::string& instance_id,
|
|
const std::string& project_id,
|
|
const std::string& organization_id,
|
|
const std::string& instance_ip,
|
|
uint16_t instance_port)
|
|
ABSL_LOCKS_EXCLUDED(sessions_mutex_);
|
|
|
|
// Starts a new streaming session to the gamelet with given |instance_id|.
|
|
// Returns a NotFound error if a session for that instance does not exists.
|
|
// Thread-safe.
|
|
absl::Status StopSession(const std::string& instance_id)
|
|
ABSL_LOCKS_EXCLUDED(sessions_mutex_);
|
|
|
|
// Returns true if there is an existing session for |instance_id|.
|
|
bool HasSessionForInstance(const std::string& instance_id)
|
|
ABSL_LOCKS_EXCLUDED(sessions_mutex_);
|
|
|
|
// Returns true if the FUSE process is up and running for an existing session
|
|
// with ID |instance_id|.
|
|
bool IsSessionHealthy(const std::string& instance_id)
|
|
ABSL_LOCKS_EXCLUDED(sessions_mutex_);
|
|
|
|
// Returns true if the MultiSession does not have any active sessions.
|
|
bool Empty() ABSL_LOCKS_EXCLUDED(sessions_mutex_);
|
|
|
|
// Returns the number of avtive sessions.
|
|
uint32_t GetSessionCount() ABSL_LOCKS_EXCLUDED(sessions_mutex_);
|
|
|
|
// For a given source directory |dir|, e.g. "C:\path\to\game", returns a
|
|
// sanitized version of |dir| plus a hash of |dir|, e.g.
|
|
// "c__path_to_game_abcdef01".
|
|
static std::string GetCacheDir(std::string dir);
|
|
|
|
// Returns the directory where manifest chunks are cached, e.g.
|
|
// "%APPDATA%\GGP\asset_streaming\c__path_to_game_abcdef01" for
|
|
// "C:\path\to\game".
|
|
// The returned path is shortened to |max_len| by removing UTF8 code points
|
|
// from the beginning of the actual cache directory (c__path...) if necessary.
|
|
static absl::StatusOr<std::string> GetCachePath(
|
|
const std::string& src_dir, size_t max_len = kDefaultMaxCachePathLen);
|
|
|
|
// Record an event associated with the multi-session.
|
|
void RecordMultiSessionEvent(metrics::DeveloperLogEvent event,
|
|
metrics::EventType code);
|
|
|
|
// Record an event for a session associated with the |instance|.
|
|
void RecordSessionEvent(metrics::DeveloperLogEvent event,
|
|
metrics::EventType code,
|
|
const std::string& instance_id);
|
|
|
|
private:
|
|
std::string src_dir_;
|
|
SessionConfig cfg_;
|
|
ProcessFactory* const process_factory_;
|
|
std::unique_ptr<DataStoreWriter> data_store_;
|
|
std::thread heartbeat_watcher_;
|
|
absl::Mutex shutdownMu_;
|
|
bool shutdown_ ABSL_GUARDED_BY(shutdownMu_) = false;
|
|
|
|
// Background thread for watching file changes and updating the manifest.
|
|
std::unique_ptr<MultiSessionRunner> runner_;
|
|
|
|
// Local forwarding port for the asset stream service.
|
|
int local_asset_stream_port_ = 0;
|
|
|
|
// Maps instance id to sessions.
|
|
std::unordered_map<std::string, std::unique_ptr<Session>> sessions_
|
|
ABSL_GUARDED_BY(sessions_mutex_);
|
|
absl::Mutex sessions_mutex_;
|
|
|
|
MultiSessionMetricsRecorder const* metrics_recorder_;
|
|
|
|
Session* FindSession(const std::string& instance_id)
|
|
ABSL_LOCKS_EXCLUDED(sessions_mutex_);
|
|
|
|
void OnContentSent(size_t byte_count, size_t chunck_count,
|
|
std::string instance_id);
|
|
|
|
void StartHeartBeatCheck();
|
|
};
|
|
|
|
} // namespace cdc_ft
|
|
|
|
#endif // ASSET_STREAM_MANAGER_MULTI_SESSION_H_
|