ifc_lite_server/routes/parse/
parquet.rs1use super::cache_keys::{
8 data_model_cache_key, has_current_data_model, has_cached_symbolic, parquet_geometry_key,
9 parquet_metadata_key, request_cache_key,
10};
11use super::replay_header::mark_header_from_cache;
12use super::{cache_symbolic_data_off_runtime, extract_file, ParseQuery};
13use crate::error::ApiError;
14use crate::services::baked_basis_zup;
15use crate::services::parquet::serialize_combined_for_layout;
16use crate::services::{extract_data_model, serialize_data_model_to_parquet};
17use crate::types::{finish_meshes, ModelMetadata, ProcessingStats};
18use ifc_lite_processing::style::ModelFinishes;
19use crate::AppState;
20use axum::{
21 body::Body,
22 extract::{Multipart, Query, State},
23 http::{header, StatusCode},
24 response::Response,
25};
26use ifc_lite_processing::{
27 extract_symbolic_data_with_provenance_in_frame, process_geometry_filtered_with_quality,
28 MeshCoordinateSpace,
29};
30use serde::{Deserialize, Serialize};
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct ParquetMetadataHeader {
35 pub cache_key: String,
36 pub metadata: ModelMetadata,
37 pub stats: ProcessingStats,
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub mesh_coordinate_space: Option<MeshCoordinateSpace>,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub site_transform: Option<Vec<f64>>,
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub building_transform: Option<Vec<f64>>,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub data_model_stats: Option<DataModelStats>,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct DataModelStats {
52 pub entity_count: usize,
53 pub property_set_count: usize,
54 pub relationship_count: usize,
55 pub spatial_node_count: usize,
56}
57
58pub async fn parse_parquet(
66 State(state): State<AppState>,
67 Query(query): Query<ParseQuery>,
68 mut multipart: Multipart,
69) -> Result<Response, ApiError> {
70 let admission_guard = state
76 .admission
77 .acquire(state.config.max_file_size_mb as u64 * 1024 * 1024)
78 .await?;
79 let data = extract_file(&mut multipart, state.config.max_file_size_mb).await?;
80
81 let tessellation_quality = query.resolved_tessellation_quality()?;
83 let cache_key = request_cache_key(&data, &query, tessellation_quality);
84
85 let layout = query.parquet_layout;
87 let parquet_cache_key = parquet_geometry_key(&cache_key, layout);
88 let metadata_cache_key = parquet_metadata_key(&cache_key);
89
90 if let (Some(cached_parquet), Some(cached_metadata_json), true, true) = (
95 state.cache.get_bytes(&parquet_cache_key).await?,
96 state.cache.get_bytes(&metadata_cache_key).await?,
97 has_current_data_model(&state.cache, &cache_key, query.data_model_entities).await,
98 has_cached_symbolic(&state.cache, &cache_key).await,
99 ) {
100 tracing::info!(
101 cache_key = %cache_key,
102 parquet_size = cached_parquet.len(),
103 "Parquet cache HIT - returning cached response"
104 );
105
106 let response = Response::builder()
109 .status(StatusCode::OK)
110 .header(header::CONTENT_TYPE, "application/x-parquet-geometry")
111 .header(
112 "X-IFC-Metadata",
113 mark_header_from_cache(
114 String::from_utf8(cached_metadata_json)
115 .map_err(|error| ApiError::Internal(error.to_string()))?,
116 ),
117 )
118 .header(header::CONTENT_LENGTH, cached_parquet.len())
119 .body(Body::from(cached_parquet))
120 .map_err(|e| ApiError::Internal(e.to_string()))?;
121
122 return Ok(response);
123 }
124
125 tracing::info!(
126 cache_key = %cache_key,
127 size = data.len(),
128 "Parquet cache MISS - processing file"
129 );
130
131 let content = data;
133
134 let serialize_start = tokio::time::Instant::now();
138 let opening_filter = query.opening_filter;
139 let data_model_entities = query.data_model_entities;
140 let (
143 (
144 ((geometry_result, mesh_count), combined_parquet),
145 (data_model_stats, data_model_parquet),
146 symbolic_data,
147 ),
148 _admission,
149 ) = tokio::task::spawn_blocking(move || {
150 let (mut geometry_result, (mut data_model, mut finishes)) = rayon::join(
153 || process_geometry_filtered_with_quality(&content, opening_filter, tessellation_quality),
154 || rayon::join(|| extract_data_model(&content), || ModelFinishes::from_content(&content)),
155 );
156 let meshes = finish_meshes(&mut finishes, std::mem::take(&mut geometry_result.meshes));
157 drop(finishes);
158
159 let dm_stats = DataModelStats {
161 entity_count: data_model.entities.len(),
162 property_set_count: data_model.property_sets.len(),
163 relationship_count: data_model.relationships.len(),
164 spatial_node_count: data_model.spatial_hierarchy.nodes.len(),
165 };
166 data_model_entities.apply(&mut data_model);
169
170 let (symbolic_data, (geo_parquet, dm_parquet)) = rayon::join(
180 || extract_symbolic_data_with_provenance_in_frame(&content, geometry_result.frame),
181 || rayon::join(
182 || {
183 let basis = baked_basis_zup(
188 Some(geometry_result.mesh_coordinate_space),
189 geometry_result.site_transform.as_deref(),
190 geometry_result.metadata.coordinate_info.origin_shift,
191 );
192 serialize_combined_for_layout(
193 &meshes,
194 layout,
195 Some(&basis),
196 )
197 },
198 || serialize_data_model_to_parquet(&data_model),
199 ),
200 );
201
202 (
203 (
204 ((geometry_result, meshes.len()), geo_parquet),
205 (dm_stats, dm_parquet),
206 symbolic_data,
207 ),
208 admission_guard,
209 )
210 })
211 .await?;
212
213 let combined_parquet = combined_parquet?;
215 let data_model_parquet = data_model_parquet?;
216
217 let serialize_time = serialize_start.elapsed();
218 tracing::info!(
219 meshes = mesh_count,
220 geometry_parquet_size = combined_parquet.len(),
221 data_model_parquet_size = data_model_parquet.len(),
222 total_serialize_time_ms = serialize_time.as_millis(),
223 "Geometry and data model serialization complete (parallel)"
224 );
225
226 let data_model_cache_key = data_model_cache_key(&cache_key, data_model_entities);
228 if let Err(e) = state
229 .cache
230 .set_bytes(&data_model_cache_key, &data_model_parquet)
231 .await
232 {
233 tracing::error!(error = %e, "Failed to cache data model");
234 } else {
235 tracing::info!(
236 cache_key = %data_model_cache_key,
237 size = data_model_parquet.len(),
238 "Data model cached (ready for client)"
239 );
240 }
241
242 cache_symbolic_data_off_runtime(state.cache.clone(), cache_key.clone(), symbolic_data).await;
245
246 let cache_key_clone = cache_key.clone();
248 let metadata_header = ParquetMetadataHeader {
249 cache_key: cache_key_clone.clone(),
250 metadata: geometry_result.metadata,
251 stats: geometry_result.stats,
252 mesh_coordinate_space: Some(geometry_result.mesh_coordinate_space),
253 site_transform: geometry_result.site_transform,
254 building_transform: geometry_result.building_transform,
255 data_model_stats: Some(data_model_stats),
256 };
257
258 let metadata_json = serde_json::to_string(&metadata_header)?;
259
260 let parquet_cache_key = parquet_geometry_key(&cache_key_clone, layout);
263 let metadata_cache_key = parquet_metadata_key(&cache_key_clone);
264 let combined_parquet_clone = combined_parquet.clone();
265 let metadata_json_clone = metadata_json.clone();
266 let cache = state.cache.clone();
267
268 tokio::spawn(async move {
274 if let Err(e) = cache
275 .set_bytes(&parquet_cache_key, &combined_parquet_clone)
276 .await
277 {
278 tracing::error!(error = %e, "Failed to cache Parquet bytes");
279 }
280 if let Err(e) = cache
281 .set_bytes(&metadata_cache_key, metadata_json_clone.as_bytes())
282 .await
283 {
284 tracing::error!(error = %e, "Failed to cache metadata");
285 }
286 tracing::info!(
287 cache_key = %cache_key_clone,
288 parquet_size = combined_parquet_clone.len(),
289 "Cached Parquet response"
290 );
291 });
292
293 let response = Response::builder()
295 .status(StatusCode::OK)
296 .header(header::CONTENT_TYPE, "application/x-parquet-geometry")
297 .header("X-IFC-Metadata", metadata_json)
298 .header(header::CONTENT_LENGTH, combined_parquet.len())
299 .body(Body::from(combined_parquet))
300 .map_err(|e| ApiError::Internal(e.to_string()))?;
301
302 Ok(response)
303}