| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you 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. |
| |
| //! A toy distributed engine, in the two halves a real one has. |
| //! |
| //! The Rust half is here: a query planner that splits the plan into stages, |
| //! the node that marks a stage, the codec that carries it, and a config |
| //! extension so the driver and its workers agree on where results go. |
| //! |
| //! The Python half is in `python/dfx_engine`: the session factory both sides |
| //! build from, the worker entry point, and the driver that fans work out. An |
| //! engine needs both, which is why this crate is a mixed maturin package |
| //! rather than a pure extension module. |
| //! |
| //! One of three libraries in `examples/distributed`. This one owns execution. |
| |
| use pyo3::exceptions::PyValueError; |
| use pyo3::prelude::*; |
| |
| use crate::config::DfxEngineConfig; |
| use crate::extension::{BundledPhysicalCodec, DfxEngineExtension}; |
| |
| mod codec; |
| mod config; |
| mod extension; |
| mod local_session; |
| mod planner; |
| mod stage; |
| |
| /// Where the results of one stage partition live. |
| /// |
| /// Exported so the Python worker writes the path the Rust node will read, |
| /// rather than the convention being spelled out on both sides of the |
| /// boundary and drifting. |
| #[pyfunction] |
| fn partition_path(shuffle_dir: &str, stage_id: u32, partition: usize) -> String { |
| stage::partition_path(shuffle_dir, stage_id, partition) |
| .to_string_lossy() |
| .into_owned() |
| } |
| |
| /// Glob matching every file [`partition_path`] can produce, for any stage. |
| /// |
| /// Exported for the same reason: the driver refuses a shuffle directory that |
| /// already holds stage output, and it has to ask that question with the same |
| /// naming convention the node answers it with. |
| #[pyfunction] |
| fn partition_glob() -> &'static str { |
| stage::PARTITION_GLOB |
| } |
| |
| /// Id of the `index`th stage in a plan, counting in pre-order from the root. |
| /// |
| /// Exported for the same reason as [`partition_path`]: a foreign node's |
| /// one-line display is replaced by the FFI wrapper's, so Python cannot read a |
| /// stage id back off a plan and has to agree with Rust about the numbering |
| /// instead. Keeping the arithmetic here means the two cannot drift. |
| #[pyfunction] |
| #[pyo3(signature = (index=0))] |
| fn stage_id(index: usize) -> PyResult<u32> { |
| u32::try_from(index) |
| .ok() |
| .and_then(|index| planner::FIRST_STAGE_ID.checked_add(index)) |
| .ok_or_else(|| PyValueError::new_err(format!("stage index {index} is out of range"))) |
| } |
| |
| #[pymodule] |
| fn _internal(m: &Bound<'_, PyModule>) -> PyResult<()> { |
| pyo3_log::init(); |
| m.add_class::<BundledPhysicalCodec>()?; |
| m.add_class::<DfxEngineConfig>()?; |
| m.add_class::<DfxEngineExtension>()?; |
| m.add_function(wrap_pyfunction!(partition_glob, m)?)?; |
| m.add_function(wrap_pyfunction!(partition_path, m)?)?; |
| m.add_function(wrap_pyfunction!(stage_id, m)?)?; |
| Ok(()) |
| } |