1515// specific language governing permissions and limitations
1616// under the License.
1717
18- use std:: collections:: HashMap ;
1918use std:: fmt;
20- use std:: sync:: atomic:: { AtomicU64 , AtomicUsize , Ordering } ;
21- use std:: sync:: { Arc , Mutex , OnceLock } ;
19+ use std:: io:: Cursor ;
20+ use std:: sync:: Arc ;
21+ use std:: sync:: atomic:: { AtomicUsize , Ordering } ;
2222
2323use arrow:: datatypes:: SchemaRef ;
24+ use arrow:: ipc:: reader:: StreamReader ;
25+ use arrow:: ipc:: writer:: StreamWriter ;
26+ use arrow:: record_batch:: RecordBatch ;
2427use datafusion:: catalog:: MemTable ;
2528use datafusion:: common:: { DataFusionError , Result , TableReference } ;
2629use datafusion:: datasource:: TableProvider ;
@@ -34,35 +37,126 @@ use pyo3::types::PyCapsule;
3437
3538use crate :: required_udf:: { TaskContextProbe , resolve_required_udf} ;
3639
37- const TABLE_PROVIDER_TOKEN : & [ u8 ] = b"DFPYEXTP" ;
38- static NEXT_TABLE_PROVIDER_ID : AtomicU64 = AtomicU64 :: new ( 1 ) ;
39- static TABLE_PROVIDERS : OnceLock < Mutex < HashMap < u64 , Arc < dyn TableProvider > > > > = OnceLock :: new ( ) ;
40+ /// Default byte prefix stamped on every table provider this codec encodes.
41+ const TABLE_PROVIDER_PREFIX : & [ u8 ] = b"DFPYEXTP" ;
4042
41- /// Hands a provider to another library in this process by token.
43+ /// Format tag that follows the prefix. Bump it if the layout below changes.
44+ const MEM_TABLE_FORMAT : & [ u8 ] = b"MEMTBL1" ;
45+
46+ /// Write a [`MemTable`] as durable metadata: its schema and every batch of
47+ /// every partition, so that a decoder anywhere can rebuild an equivalent
48+ /// table from the bytes alone.
4249///
43- /// Encoding inserts, decoding removes. Two consequences worth knowing before
44- /// copying this:
50+ /// Layout, after the caller's provider prefix:
4551///
46- /// - **Decode consumes the token.** Decoding the same encoded bytes twice
47- /// fails the second time with `Unknown ... table provider token`. That is
48- /// fine here because every plan is encoded immediately before the single
49- /// decode that consumes it, but it rules out anything that replays a stored
50- /// plan, retries a decode, or fans one encoded plan out to several readers.
51- /// - **An encode that is never decoded leaks.** Nothing expires entries, so a
52- /// plan that fails to reach its decoder keeps its provider alive for the
53- /// life of the process.
52+ /// ```text
53+ /// b"MEMTBL1" | u32 LE n_partitions | { u32 LE ipc_len | ipc stream }*
54+ /// ```
5455///
55- /// Both are acceptable for an example whose job is to show that Rust type
56- /// identity survives a trip through two other libraries. Neither is acceptable
57- /// in a real codec, which should encode metadata sufficient to rebuild the
58- /// provider rather than parking the object here.
59- fn table_providers ( ) -> & ' static Mutex < HashMap < u64 , Arc < dyn TableProvider > > > {
60- TABLE_PROVIDERS . get_or_init ( || Mutex :: new ( HashMap :: new ( ) ) )
56+ /// Each partition is one Arrow IPC stream. The stream carries the schema, so
57+ /// the decoder never has to trust a schema handed to it out of band.
58+ fn encode_mem_table ( table : & MemTable , buf : & mut Vec < u8 > ) -> Result < ( ) > {
59+ let schema = table. schema ( ) ;
60+ buf. extend_from_slice ( MEM_TABLE_FORMAT ) ;
61+ buf. extend_from_slice ( & length_prefix ( table. batches . len ( ) ) ?) ;
62+
63+ for partition in & table. batches {
64+ // `MemTable` guards each partition with a tokio `RwLock`. This encode
65+ // runs on a tokio worker thread, where `blocking_read` panics, so
66+ // take the lock only if it is free. A partition that is mid-insert
67+ // is reported rather than waited for.
68+ let batches = partition. try_read ( ) . map_err ( |_| {
69+ DataFusionError :: Internal (
70+ "datafusion-ffi-example cannot encode a MemTable while a partition is locked"
71+ . to_string ( ) ,
72+ )
73+ } ) ?;
74+
75+ let mut ipc = Vec :: new ( ) ;
76+ let mut writer = StreamWriter :: try_new ( & mut ipc, schema. as_ref ( ) ) ?;
77+ for batch in batches. iter ( ) {
78+ writer. write ( batch) ?;
79+ }
80+ writer. finish ( ) ?;
81+ drop ( writer) ;
82+
83+ buf. extend_from_slice ( & length_prefix ( ipc. len ( ) ) ?) ;
84+ buf. extend_from_slice ( & ipc) ;
85+ }
86+ Ok ( ( ) )
6187}
6288
63- fn token_id ( buf : & [ u8 ] , prefix : & [ u8 ] ) -> Option < u64 > {
64- let id: [ u8 ; 8 ] = buf. strip_prefix ( prefix) ?. try_into ( ) . ok ( ) ?;
65- Some ( u64:: from_le_bytes ( id) )
89+ /// Rebuild a [`MemTable`] from bytes written by [`encode_mem_table`].
90+ ///
91+ /// The table's schema is the one carried inside the IPC streams, not the
92+ /// `schema` argument DataFusion passes to `try_decode_table_provider`. A
93+ /// payload that does not describe itself consistently is rejected here
94+ /// instead of producing a table whose batches disagree with its schema.
95+ fn decode_mem_table ( payload : & [ u8 ] ) -> Result < MemTable > {
96+ let mut rest = payload. strip_prefix ( MEM_TABLE_FORMAT ) . ok_or_else ( || {
97+ DataFusionError :: Internal (
98+ "datafusion-ffi-example table provider payload has an unknown format tag" . to_string ( ) ,
99+ )
100+ } ) ?;
101+
102+ let n_partitions = read_length_prefix ( & mut rest) ?;
103+ let mut schema: Option < SchemaRef > = None ;
104+ let mut partitions: Vec < Vec < RecordBatch > > = Vec :: with_capacity ( n_partitions) ;
105+
106+ for _ in 0 ..n_partitions {
107+ let ipc_len = read_length_prefix ( & mut rest) ?;
108+ if rest. len ( ) < ipc_len {
109+ return Err ( DataFusionError :: Internal (
110+ "datafusion-ffi-example table provider payload is truncated" . to_string ( ) ,
111+ ) ) ;
112+ }
113+ let ( ipc, tail) = rest. split_at ( ipc_len) ;
114+ rest = tail;
115+
116+ let reader = StreamReader :: try_new ( Cursor :: new ( ipc) , None ) ?;
117+ let ipc_schema = reader. schema ( ) ;
118+ match & schema {
119+ None => schema = Some ( ipc_schema) ,
120+ Some ( first) if * first != ipc_schema => {
121+ return Err ( DataFusionError :: Internal (
122+ "datafusion-ffi-example table provider partitions disagree on schema"
123+ . to_string ( ) ,
124+ ) ) ;
125+ }
126+ Some ( _) => { }
127+ }
128+ partitions. push ( reader. collect :: < std:: result:: Result < Vec < _ > , _ > > ( ) ?) ;
129+ }
130+
131+ if !rest. is_empty ( ) {
132+ return Err ( DataFusionError :: Internal (
133+ "datafusion-ffi-example table provider payload has trailing bytes" . to_string ( ) ,
134+ ) ) ;
135+ }
136+
137+ let schema = schema. ok_or_else ( || {
138+ DataFusionError :: Internal (
139+ "datafusion-ffi-example table provider payload has no partitions" . to_string ( ) ,
140+ )
141+ } ) ?;
142+ MemTable :: try_new ( schema, partitions)
143+ }
144+
145+ fn length_prefix ( len : usize ) -> Result < [ u8 ; 4 ] > {
146+ u32:: try_from ( len)
147+ . map ( u32:: to_le_bytes)
148+ . map_err ( |_| DataFusionError :: Internal ( format ! ( "length {len} does not fit in u32" ) ) )
149+ }
150+
151+ fn read_length_prefix ( rest : & mut & [ u8 ] ) -> Result < usize > {
152+ let ( head, tail) = rest. split_at_checked ( 4 ) . ok_or_else ( || {
153+ DataFusionError :: Internal (
154+ "datafusion-ffi-example table provider payload is truncated" . to_string ( ) ,
155+ )
156+ } ) ?;
157+ * rest = tail;
158+ let bytes: [ u8 ; 4 ] = head. try_into ( ) . expect ( "split_at_checked returned 4 bytes" ) ;
159+ Ok ( u32:: from_le_bytes ( bytes) as usize )
66160}
67161
68162#[ derive( Debug , Default ) ]
@@ -76,23 +170,20 @@ pub(crate) struct CallCounters {
76170
77171/// Example codec for objects owned by this extension library.
78172///
79- /// The table-provider token registry is intentionally process-local. It is a compact
80- /// example of preserving Rust type identity across three loaded libraries, not a
81- /// network serialization format. Production libraries should encode reconstructible
82- /// provider metadata rather than retaining objects in a global registry.
83- ///
84- /// See [`table_providers`] for the token lifecycle, which is narrower than it
85- /// looks: a decode consumes its token, so the same encoded plan cannot be
86- /// decoded twice.
173+ /// Table providers are encoded as durable metadata, see [`encode_mem_table`].
174+ /// Nothing is retained between encode and decode, so the same bytes decode
175+ /// any number of times, in any process, and an encoded plan that never
176+ /// reaches a decoder costs nothing.
87177struct CountingLogicalExtensionCodec {
88178 inner : DefaultLogicalExtensionCodec ,
89179 counters : Arc < CallCounters > ,
90180 /// Scalar function every table-provider decode must resolve from the
91181 /// `TaskContext` it is handed. See [`crate::required_udf`].
92182 required_udf : Option < String > ,
93- /// Byte prefix identifying providers this codec owns. Distinct tokens let a
94- /// test install several instances and observe which one the chain picks.
95- token : Arc < [ u8 ] > ,
183+ /// Byte prefix identifying providers this codec owns. Distinct prefixes
184+ /// let a test install several instances and observe which one the chain
185+ /// picks.
186+ provider_prefix : Arc < [ u8 ] > ,
96187}
97188
98189impl fmt:: Debug for CountingLogicalExtensionCodec {
@@ -127,19 +218,11 @@ impl LogicalExtensionCodec for CountingLogicalExtensionCodec {
127218 ctx : & TaskContext ,
128219 ) -> Result < Arc < dyn TableProvider > > {
129220 resolve_required_udf ( self . required_udf . as_deref ( ) , ctx, & self . counters . task_ctx ) ?;
130- if let Some ( id ) = token_id ( buf, & self . token ) {
221+ if let Some ( payload ) = buf. strip_prefix ( self . provider_prefix . as_ref ( ) ) {
131222 self . counters
132223 . decode_table_provider
133224 . fetch_add ( 1 , Ordering :: SeqCst ) ;
134- return table_providers ( )
135- . lock ( )
136- . map_err ( |err| DataFusionError :: Internal ( err. to_string ( ) ) ) ?
137- . remove ( & id)
138- . ok_or_else ( || {
139- DataFusionError :: Internal ( format ! (
140- "Unknown datafusion-ffi-example table provider token {id}"
141- ) )
142- } ) ;
225+ return Ok ( Arc :: new ( decode_mem_table ( payload) ?) ) ;
143226 }
144227 self . inner
145228 . try_decode_table_provider ( buf, table_ref, schema, ctx)
@@ -151,18 +234,12 @@ impl LogicalExtensionCodec for CountingLogicalExtensionCodec {
151234 node : Arc < dyn TableProvider > ,
152235 buf : & mut Vec < u8 > ,
153236 ) -> Result < ( ) > {
154- if node. downcast_ref :: < MemTable > ( ) . is_some ( ) {
237+ if let Some ( table ) = node. downcast_ref :: < MemTable > ( ) {
155238 self . counters
156239 . encode_table_provider
157240 . fetch_add ( 1 , Ordering :: SeqCst ) ;
158- let id = NEXT_TABLE_PROVIDER_ID . fetch_add ( 1 , Ordering :: SeqCst ) ;
159- table_providers ( )
160- . lock ( )
161- . map_err ( |err| DataFusionError :: Internal ( err. to_string ( ) ) ) ?
162- . insert ( id, node) ;
163- buf. extend_from_slice ( & self . token ) ;
164- buf. extend_from_slice ( & id. to_le_bytes ( ) ) ;
165- return Ok ( ( ) ) ;
241+ buf. extend_from_slice ( & self . provider_prefix ) ;
242+ return encode_mem_table ( table, buf) ;
166243 }
167244 self . inner . try_encode_table_provider ( table_ref, node, buf)
168245 }
@@ -188,7 +265,7 @@ impl LogicalExtensionCodec for CountingLogicalExtensionCodec {
188265pub ( crate ) struct MyLogicalExtensionCodec {
189266 counters : Arc < CallCounters > ,
190267 required_udf : Option < String > ,
191- token : Arc < [ u8 ] > ,
268+ provider_prefix : Arc < [ u8 ] > ,
192269}
193270
194271#[ pymethods]
@@ -200,7 +277,7 @@ impl MyLogicalExtensionCodec {
200277 /// unset for the ordinary behaviour; set it to observe *which* session's
201278 /// registry the FFI decode callback actually receives.
202279 ///
203- /// `provider_prefix` overrides [`TABLE_PROVIDER_TOKEN `], the byte prefix
280+ /// `provider_prefix` overrides [`TABLE_PROVIDER_PREFIX `], the byte prefix
204281 /// stamped on encoded table providers. Two instances built with different
205282 /// prefixes each own a disjoint slice of the wire format, which is what
206283 /// lets a test install both and tell from the decoded bytes which one the
@@ -211,8 +288,8 @@ impl MyLogicalExtensionCodec {
211288 Self {
212289 counters : Arc :: new ( CallCounters :: default ( ) ) ,
213290 required_udf : require_udf_on_decode,
214- token : provider_prefix. map_or_else (
215- || Arc :: from ( TABLE_PROVIDER_TOKEN ) ,
291+ provider_prefix : provider_prefix. map_or_else (
292+ || Arc :: from ( TABLE_PROVIDER_PREFIX ) ,
216293 |prefix| Arc :: from ( prefix. as_bytes ( ) ) ,
217294 ) ,
218295 }
@@ -259,7 +336,7 @@ impl MyLogicalExtensionCodec {
259336 inner : DefaultLogicalExtensionCodec { } ,
260337 counters : Arc :: clone ( & self . counters ) ,
261338 required_udf : self . required_udf . clone ( ) ,
262- token : Arc :: clone ( & self . token ) ,
339+ provider_prefix : Arc :: clone ( & self . provider_prefix ) ,
263340 } ) ;
264341
265342 let runtime = get_tokio_runtime ( ) . handle ( ) . clone ( ) ;
0 commit comments