// Copyright Epic Games, Inc. All Rights Reserved. #include "httpbuildstore.h" #include #include #include #include #include #include #include #include #include #include namespace zen { using namespace std::literals; ZEN_DEFINE_LOG_CATEGORY_STATIC(LogBuilds, "builds"sv); HttpBuildStoreService::HttpBuildStoreService(HttpStatusService& StatusService, HttpStatsService& StatsService, BuildStore& Store) : m_Log(logging::Get("builds")) , m_StatusService(StatusService) , m_StatsService(StatsService) , m_BuildStore(Store) { Initialize(); m_StatusService.RegisterHandler("builds", *this); m_StatsService.RegisterHandler("builds", *this); } HttpBuildStoreService::~HttpBuildStoreService() { m_StatsService.UnregisterHandler("builds", *this); m_StatusService.UnregisterHandler("builds", *this); } const char* HttpBuildStoreService::BaseUri() const { return "/builds/"; } void HttpBuildStoreService::Initialize() { ZEN_LOG_INFO(LogBuilds, "Initializing Builds Service"); static constexpr AsciiSet ValidNamespaceCharactersSet{"abcdefghijklmnopqrstuvwxyz0123456789-_.ABCDEFGHIJKLMNOPQRSTUVWXYZ"}; static constexpr AsciiSet ValidBucketCharactersSet{"abcdefghijklmnopqrstuvwxyz0123456789-_.ABCDEFGHIJKLMNOPQRSTUVWXYZ"}; static constexpr AsciiSet ValidHexCharactersSet{"0123456789abcdefABCDEF"}; m_Router.AddMatcher("namespace", [](std::string_view Str) -> bool { return !Str.empty() && AsciiSet::HasOnly(Str, ValidNamespaceCharactersSet); }); m_Router.AddMatcher("bucket", [](std::string_view Str) -> bool { return !Str.empty() && AsciiSet::HasOnly(Str, ValidBucketCharactersSet); }); m_Router.AddMatcher("buildid", [](std::string_view Str) -> bool { return Str.length() == Oid::StringLength && AsciiSet::HasOnly(Str, ValidHexCharactersSet); }); m_Router.AddMatcher("hash", [](std::string_view Str) -> bool { return Str.length() == IoHash::StringLength && AsciiSet::HasOnly(Str, ValidHexCharactersSet); }); m_Router.RegisterRoute( "{namespace}/{bucket}/{buildid}/blobs/{hash}", [this](HttpRouterRequest& Req) { PutBlobRequest(Req); }, HttpVerb::kPut); m_Router.RegisterRoute( "{namespace}/{bucket}/{buildid}/blobs/{hash}", [this](HttpRouterRequest& Req) { GetBlobRequest(Req); }, HttpVerb::kGet | HttpVerb::kPost); m_Router.RegisterRoute( "{namespace}/{bucket}/{buildid}/blobs/putBlobMetadata", [this](HttpRouterRequest& Req) { PutMetadataRequest(Req); }, HttpVerb::kPost); m_Router.RegisterRoute( "{namespace}/{bucket}/{buildid}/blobs/getBlobMetadata", [this](HttpRouterRequest& Req) { GetMetadatasRequest(Req); }, HttpVerb::kPost); m_Router.RegisterRoute( "{namespace}/{bucket}/{buildid}/blobs/exists", [this](HttpRouterRequest& Req) { BlobsExistsRequest(Req); }, HttpVerb::kPost); } void HttpBuildStoreService::HandleRequest(zen::HttpServerRequest& Request) { ZEN_TRACE_CPU("HttpBuildStoreService::HandleRequest"); metrics::OperationTiming::Scope $(m_HttpRequests); m_BuildStoreStats.RequestCount++; if (m_Router.HandleRequest(Request) == false) { ZEN_LOG_WARN(LogBuilds, "No route found for {0}", Request.RelativeUri()); return Request.WriteResponse(HttpResponseCode::NotFound, HttpContentType::kText, "Not found"sv); } } void HttpBuildStoreService::PutBlobRequest(HttpRouterRequest& Req) { ZEN_TRACE_CPU("HttpBuildStoreService::PutBlobRequest"); HttpServerRequest& ServerRequest = Req.ServerRequest(); const std::string_view Namespace = Req.GetCapture(1); const std::string_view Bucket = Req.GetCapture(2); const std::string_view BuildId = Req.GetCapture(3); const std::string_view Hash = Req.GetCapture(4); ZEN_UNUSED(Namespace, Bucket, BuildId); IoHash BlobHash; if (!IoHash::TryParse(Hash, BlobHash)) { m_BuildStoreStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Invalid blob hash '{}'", Hash)); } m_BuildStoreStats.BlobWriteCount++; IoBuffer Payload = ServerRequest.ReadPayload(); if (!Payload) { m_BuildStoreStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Payload blob {} is empty", Hash)); } if (Payload.GetContentType() != HttpContentType::kCompressedBinary) { m_BuildStoreStats.BadRequestCount++; return ServerRequest.WriteResponse( HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Payload blob {} content type {} is invalid", Hash, ToString(Payload.GetContentType()))); } m_BuildStore.PutBlob(BlobHash, ServerRequest.ReadPayload()); // ZEN_INFO("Stored blob {}. Size: {}", BlobHash, ServerRequest.ReadPayload().GetSize()); return ServerRequest.WriteResponse(HttpResponseCode::OK); } void HttpBuildStoreService::GetBlobRequest(HttpRouterRequest& Req) { ZEN_TRACE_CPU("HttpBuildStoreService::GetBlobRequest"); HttpServerRequest& ServerRequest = Req.ServerRequest(); std::string_view Namespace = Req.GetCapture(1); std::string_view Bucket = Req.GetCapture(2); std::string_view BuildId = Req.GetCapture(3); std::string_view Hash = Req.GetCapture(4); ZEN_UNUSED(Namespace, Bucket, BuildId); IoHash BlobHash; if (!IoHash::TryParse(Hash, BlobHash)) { m_BuildStoreStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Invalid blob hash '{}'", Hash)); } std::vector> OffsetAndLengthPairs; if (ServerRequest.RequestVerb() == HttpVerb::kPost) { CbObject RangePayload = ServerRequest.ReadPayloadObject(); if (RangePayload) { CbArrayView RangesArray = RangePayload["ranges"sv].AsArrayView(); OffsetAndLengthPairs.reserve(RangesArray.Num()); for (CbFieldView FieldView : RangesArray) { CbObjectView RangeView = FieldView.AsObjectView(); uint64_t RangeOffset = RangeView["offset"sv].AsUInt64(); uint64_t RangeLength = RangeView["length"sv].AsUInt64(); OffsetAndLengthPairs.push_back(std::make_pair(RangeOffset, RangeLength)); } } if (OffsetAndLengthPairs.empty()) { m_BuildStoreStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, "Fetching blob without ranges must be done with the GET verb"); } } else { HttpRanges Ranges; bool HasRange = ServerRequest.TryGetRanges(Ranges); if (HasRange) { if (Ranges.size() > 1) { // Only a single http range is supported, we have limited support for http multirange responses m_BuildStoreStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Multiple ranges in blob request is only supported for {} accept type", ToString(HttpContentType::kCbPackage))); } const HttpRange& FirstRange = Ranges.front(); OffsetAndLengthPairs.push_back(std::make_pair(FirstRange.Start, FirstRange.End - FirstRange.Start + 1)); } } m_BuildStoreStats.BlobReadCount++; IoBuffer Blob = m_BuildStore.GetBlob(BlobHash); if (!Blob) { return ServerRequest.WriteResponse(HttpResponseCode::NotFound, HttpContentType::kText, fmt::format("Blob with hash '{}' could not be found", Hash)); } m_BuildStoreStats.BlobHitCount++; if (OffsetAndLengthPairs.empty()) { return ServerRequest.WriteResponse(HttpResponseCode::OK, Blob.GetContentType(), Blob); } if (ServerRequest.AcceptContentType() == HttpContentType::kCbPackage) { const uint64_t BlobSize = Blob.GetSize(); CbPackage ResponsePackage; std::vector RangeBuffers; CbObjectWriter Writer; Writer.BeginArray("ranges"sv); for (const std::pair& Range : OffsetAndLengthPairs) { const uint64_t MaxBlobSize = Range.first < BlobSize ? BlobSize - Range.first : 0; const uint64_t RangeSize = Min(Range.second, MaxBlobSize); if (Range.first + RangeSize <= BlobSize) { RangeBuffers.push_back(IoBuffer(Blob, Range.first, RangeSize)); Writer.BeginObject(); { Writer.AddInteger("offset"sv, Range.first); Writer.AddInteger("length"sv, RangeSize); } Writer.EndObject(); } } Writer.EndArray(); CompositeBuffer Ranges(RangeBuffers); CbAttachment PayloadAttachment(std::move(Ranges), BlobHash); Writer.AddAttachment("payload", PayloadAttachment); CbObject HeaderObject = Writer.Save(); ResponsePackage.AddAttachment(PayloadAttachment); ResponsePackage.SetObject(HeaderObject); CompositeBuffer RpcResponseBuffer = FormatPackageMessageBuffer(ResponsePackage); uint64_t ResponseSize = RpcResponseBuffer.GetSize(); ZEN_UNUSED(ResponseSize); return ServerRequest.WriteResponse(HttpResponseCode::OK, HttpContentType::kCbPackage, RpcResponseBuffer); } else { ZEN_ASSERT(OffsetAndLengthPairs.size() == 1); const std::pair& OffsetAndLength = OffsetAndLengthPairs.front(); const uint64_t BlobSize = Blob.GetSize(); const uint64_t MaxBlobSize = OffsetAndLength.first < BlobSize ? BlobSize - OffsetAndLength.first : 0; const uint64_t RangeSize = Min(OffsetAndLength.second, MaxBlobSize); if (OffsetAndLength.first + RangeSize > BlobSize) { return ServerRequest.WriteResponse(HttpResponseCode::NoContent); } Blob = IoBuffer(Blob, OffsetAndLength.first, RangeSize); return ServerRequest.WriteResponse(HttpResponseCode::OK, ZenContentType::kBinary, Blob); } } void HttpBuildStoreService::PutMetadataRequest(HttpRouterRequest& Req) { ZEN_TRACE_CPU("HttpBuildStoreService::PutMetadataRequest"); HttpServerRequest& ServerRequest = Req.ServerRequest(); std::string_view Namespace = Req.GetCapture(1); std::string_view Bucket = Req.GetCapture(2); std::string_view BuildId = Req.GetCapture(3); ZEN_UNUSED(Namespace, Bucket, BuildId); IoBuffer MetaPayload = ServerRequest.ReadPayload(); if (MetaPayload.GetContentType() != ZenContentType::kCbPackage) { throw std::runtime_error(fmt::format("PutMetadataRequest payload has unexpected payload type '{}', expected '{}'", ToString(MetaPayload.GetContentType()), ToString(ZenContentType::kCbPackage))); } CbPackage Message = ParsePackageMessage(MetaPayload); CbObjectView MessageObject = Message.GetObject(); if (!MessageObject) { throw std::runtime_error("PutMetadataRequest payload object is missing"); } CbArrayView BlobsArray = MessageObject["blobHashes"sv].AsArrayView(); CbArrayView MetadataArray = MessageObject["metadatas"sv].AsArrayView(); const uint64_t BlobCount = BlobsArray.Num(); if (BlobCount == 0) { throw std::runtime_error("PutMetadataRequest blobs array is empty"); } if (BlobCount != MetadataArray.Num()) { throw std::runtime_error( fmt::format("PutMetadataRequest metadata array size {} does not match blobs array size {}", MetadataArray.Num(), BlobCount)); } std::vector BlobHashes; std::vector MetadataPayloads; BlobHashes.reserve(BlobCount); MetadataPayloads.reserve(BlobCount); auto BlobsArrayIt = begin(BlobsArray); auto MetadataArrayIt = begin(MetadataArray); while (BlobsArrayIt != end(BlobsArray)) { const IoHash BlobHash = (*BlobsArrayIt).AsHash(); const IoHash MetadataHash = (*MetadataArrayIt).AsAttachment(); const CbAttachment* Attachment = Message.FindAttachment(MetadataHash); if (Attachment == nullptr) { throw std::runtime_error(fmt::format("Blob metadata attachment {} is missing", MetadataHash)); } BlobHashes.push_back(BlobHash); if (Attachment->IsObject()) { MetadataPayloads.push_back(Attachment->AsObject().GetBuffer().MakeOwned().AsIoBuffer()); MetadataPayloads.back().SetContentType(ZenContentType::kCbObject); } else if (Attachment->IsCompressedBinary()) { MetadataPayloads.push_back(Attachment->AsCompressedBinary().GetCompressed().Flatten().AsIoBuffer()); MetadataPayloads.back().SetContentType(ZenContentType::kCompressedBinary); } else { ZEN_ASSERT(Attachment->IsBinary()); MetadataPayloads.push_back(Attachment->AsBinary().AsIoBuffer()); MetadataPayloads.back().SetContentType(ZenContentType::kBinary); } BlobsArrayIt++; MetadataArrayIt++; } m_BuildStore.PutMetadatas(BlobHashes, MetadataPayloads, &GetSmallWorkerPool(EWorkloadType::Burst)); return ServerRequest.WriteResponse(HttpResponseCode::OK); } void HttpBuildStoreService::GetMetadatasRequest(HttpRouterRequest& Req) { ZEN_TRACE_CPU("HttpBuildStoreService::GetMetadatasRequest"); HttpServerRequest& ServerRequest = Req.ServerRequest(); std::string_view Namespace = Req.GetCapture(1); std::string_view Bucket = Req.GetCapture(2); std::string_view BuildId = Req.GetCapture(3); ZEN_UNUSED(Namespace, Bucket, BuildId); IoBuffer RequestPayload = ServerRequest.ReadPayload(); if (!RequestPayload) { m_BuildStoreStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, "Expected compact binary body for metadata request, body is missing"); } if (RequestPayload.GetContentType() != HttpContentType::kCbObject) { m_BuildStoreStats.BadRequestCount++; return ServerRequest.WriteResponse( HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Expected compact binary body for metadata request, got {}", ToString(RequestPayload.GetContentType()))); } if (CbValidateError ValidateError = ValidateCompactBinary(RequestPayload.GetView(), CbValidateMode::Default); ValidateError != CbValidateError::None) { m_BuildStoreStats.BadRequestCount++; return ServerRequest.WriteResponse( HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Compact binary body for metadata request is not valid, reason: {}", ToString(ValidateError))); } CbObject RequestObject = LoadCompactBinaryObject(RequestPayload); CbArrayView BlobsArray = RequestObject["blobHashes"sv].AsArrayView(); if (!BlobsArray) { m_BuildStoreStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, "Compact binary body for metadata request is missing 'blobHashes' array"); } const uint64_t BlobCount = BlobsArray.Num(); std::vector BlobRawHashes; BlobRawHashes.reserve(BlobCount); for (CbFieldView BlockHashView : BlobsArray) { BlobRawHashes.push_back(BlockHashView.AsHash()); if (BlobRawHashes.back() == IoHash::Zero) { const uint8_t Type = (uint8_t)BlockHashView.GetValue().GetType(); return ServerRequest.WriteResponse( HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Compact binary body for metadata 'blobHashes' array contains invalid field type: {}", Type)); } } m_BuildStoreStats.BlobMetaReadCount += BlobRawHashes.size(); std::vector BlockMetadatas = m_BuildStore.GetMetadatas(BlobRawHashes, &GetSmallWorkerPool(EWorkloadType::Burst)); CbPackage ResponsePackage; std::vector Attachments; tsl::robin_set AttachmentHashes; Attachments.reserve(BlobCount); AttachmentHashes.reserve(BlobCount); { CbObjectWriter ResponseWriter; ResponseWriter.BeginArray("blobHashes"); for (size_t BlockHashIndex = 0; BlockHashIndex < BlobRawHashes.size(); BlockHashIndex++) { if (BlockMetadatas[BlockHashIndex]) { const IoHash& BlockHash = BlobRawHashes[BlockHashIndex]; ResponseWriter.AddHash(BlockHash); } } ResponseWriter.EndArray(); // blobHashes ResponseWriter.BeginArray("metadatas"); for (size_t BlockHashIndex = 0; BlockHashIndex < BlobRawHashes.size(); BlockHashIndex++) { if (IoBuffer Metadata = BlockMetadatas[BlockHashIndex]; Metadata) { switch (Metadata.GetContentType()) { case ZenContentType::kCbObject: { CbObject Object = CbObject(SharedBuffer(std::move(Metadata)).MakeOwned()); const IoHash ObjectHash = Object.GetHash(); ResponseWriter.AddBinaryAttachment(ObjectHash); if (!AttachmentHashes.contains(ObjectHash)) { Attachments.push_back(CbAttachment(Object, ObjectHash)); AttachmentHashes.insert(ObjectHash); } } break; case ZenContentType::kCompressedBinary: { IoHash RawHash; uint64_t _; CompressedBuffer Compressed = CompressedBuffer::FromCompressed(SharedBuffer(std::move(Metadata)), RawHash, _); ResponseWriter.AddBinaryAttachment(RawHash); if (!AttachmentHashes.contains(RawHash)) { Attachments.push_back(CbAttachment(Compressed, RawHash)); AttachmentHashes.insert(RawHash); } } break; default: { const IoHash RawHash = IoHash::HashBuffer(Metadata); ResponseWriter.AddBinaryAttachment(RawHash); if (!AttachmentHashes.contains(RawHash)) { Attachments.push_back(CbAttachment(SharedBuffer(Metadata), RawHash)); AttachmentHashes.insert(RawHash); } } break; } } } ResponseWriter.EndArray(); // metadatas ResponsePackage.SetObject(ResponseWriter.Save()); } ResponsePackage.AddAttachments(Attachments); CompositeBuffer RpcResponseBuffer = FormatPackageMessageBuffer(ResponsePackage); ServerRequest.WriteResponse(HttpResponseCode::OK, HttpContentType::kCbPackage, RpcResponseBuffer); } void HttpBuildStoreService::BlobsExistsRequest(HttpRouterRequest& Req) { ZEN_TRACE_CPU("HttpBuildStoreService::BlobsExistsRequest"); HttpServerRequest& ServerRequest = Req.ServerRequest(); std::string_view Namespace = Req.GetCapture(1); std::string_view Bucket = Req.GetCapture(2); std::string_view BuildId = Req.GetCapture(3); ZEN_UNUSED(Namespace, Bucket, BuildId); IoBuffer RequestPayload = ServerRequest.ReadPayload(); if (!RequestPayload) { m_BuildStoreStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, "Expected compact binary body for blob exists request, body is missing"); } if (RequestPayload.GetContentType() != HttpContentType::kCbObject) { m_BuildStoreStats.BadRequestCount++; return ServerRequest.WriteResponse( HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Expected compact binary body for blob exists request, got {}", ToString(RequestPayload.GetContentType()))); } if (CbValidateError ValidateError = ValidateCompactBinary(RequestPayload.GetView(), CbValidateMode::Default); ValidateError != CbValidateError::None) { m_BuildStoreStats.BadRequestCount++; return ServerRequest.WriteResponse( HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Compact binary body for blob exists request is not valid, reason: {}", ToString(ValidateError))); } CbObject RequestObject = LoadCompactBinaryObject(RequestPayload); CbArrayView BlobsArray = RequestObject["blobHashes"sv].AsArrayView(); if (!BlobsArray) { m_BuildStoreStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, "Compact binary body for blob exists request is missing 'blobHashes' array"); } std::vector BlobRawHashes; BlobRawHashes.reserve(BlobsArray.Num()); for (CbFieldView BlockHashView : BlobsArray) { BlobRawHashes.push_back(BlockHashView.AsHash()); if (BlobRawHashes.back() == IoHash::Zero) { const uint8_t Type = (uint8_t)BlockHashView.GetValue().GetType(); return ServerRequest.WriteResponse( HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Compact binary body for blob exists request 'blobHashes' array contains invalid field type: {}", Type)); } } m_BuildStoreStats.BlobExistsCount += BlobRawHashes.size(); std::vector BlobsExists = m_BuildStore.BlobsExists(BlobRawHashes); CbObjectWriter ResponseWriter(9 * BlobsExists.size()); ResponseWriter.BeginArray("blobExists"sv); for (const BuildStore::BlobExistsResult& BlobExists : BlobsExists) { ResponseWriter.AddBool(BlobExists.HasBody); if (BlobExists.HasBody) { m_BuildStoreStats.BlobExistsBodyHitCount++; } } ResponseWriter.EndArray(); // blobExist ResponseWriter.BeginArray("metadataExists"sv); for (const BuildStore::BlobExistsResult& BlobExists : BlobsExists) { ResponseWriter.AddBool(BlobExists.HasMetadata); if (BlobExists.HasMetadata) { m_BuildStoreStats.BlobExistsMetaHitCount++; } } ResponseWriter.EndArray(); // metadataExists CbObject ResponseObject = ResponseWriter.Save(); return ServerRequest.WriteResponse(HttpResponseCode::OK, ResponseObject); } void HttpBuildStoreService::HandleStatsRequest(HttpServerRequest& Request) { ZEN_TRACE_CPU("HttpBuildStoreService::Stats"); BuildStore::StorageStats StorageStats = m_BuildStore.GetStorageStats(); CbObjectWriter Cbo; EmitSnapshot("requests", m_HttpRequests, Cbo); Cbo.BeginObject("store"); { Cbo.BeginObject("size"); { Cbo << "disk" << StorageStats.BlobLogByteCount + StorageStats.MetadataLogByteCount; Cbo << "memory" << 0u; } Cbo.EndObject(); Cbo.BeginObject("blobs"); { Cbo << "readcount" << m_BuildStoreStats.BlobReadCount << "writecount" << m_BuildStoreStats.BlobWriteCount << "hitcount" << m_BuildStoreStats.BlobHitCount; Cbo << "count" << StorageStats.EntryCount; Cbo << "bytes" << StorageStats.BlobBytes; } Cbo.EndObject(); Cbo.BeginObject("metadata"); { Cbo << "readcount" << m_BuildStoreStats.BlobMetaReadCount << "writecount" << m_BuildStoreStats.BlobMetaWriteCount << "hitcount" << m_BuildStoreStats.BlobMetaHitCount; Cbo << "count" << StorageStats.MetadataCount; Cbo << "bytes" << StorageStats.MetadataByteCount; } Cbo.EndObject(); Cbo << "requestcount" << m_BuildStoreStats.RequestCount; Cbo << "badrequestcount" << m_BuildStoreStats.BadRequestCount; } Cbo.EndObject(); Cbo.BeginObject("cid"); { Cbo.BeginObject("size"); { Cbo << "blobs" << StorageStats.BlobBytes; Cbo << "metadata" << StorageStats.MetadataByteCount; Cbo << "total" << StorageStats.BlobBytes + StorageStats.MetadataByteCount; } Cbo.EndObject(); } Cbo.EndObject(); return Request.WriteResponse(HttpResponseCode::OK, Cbo.Save()); } void HttpBuildStoreService::HandleStatusRequest(HttpServerRequest& Request) { ZEN_TRACE_CPU("HttpBuildStoreService::Status"); CbObjectWriter Cbo; Cbo << "ok" << true; Request.WriteResponse(HttpResponseCode::OK, Cbo.Save()); } } // namespace zen