3939 }
4040
4141 WriterPool struct {
42- configMutex sync.Mutex
4342 stats * Stats
44- config any
43+ lifecycle Writer // adapter owning destination-level resources; reused for Clear/Close (no re-parse)
44+ parsedConfig Config // destination config parsed once; shared by every writer thread (read-only after parse)
4545 init NewFunc
4646 writerSchema sync.Map
4747 batchSize int64
@@ -90,19 +90,39 @@ func WithApplyFilter(applyFilter bool) ThreadOptions {
9090 }
9191}
9292
93+ // NewWriterPool builds a writer pool for a destination. It owns the lifecycle of
94+ // destination-level process resources: it starts them up front (e.g. the Iceberg
95+ // shared JVM, via Initializable), validates the connection (Check), and exposes
96+ // Close to tear them down. Call pool.Close() when done (defer it right after a
97+ // successful NewWriterPool).
9398func NewWriterPool (ctx context.Context , config * types.WriterConfig , syncStreams []string , batchSize int64 ) (* WriterPool , error ) {
9499 newfunc , found := RegisteredWriters [config .Type ]
95100 if ! found {
96101 return nil , fmt .Errorf ("invalid destination type has been passed [%s]" , config .Type )
97102 }
98103
104+ // Parse the destination config exactly once into a shared instance. Check may
105+ // finalize it (e.g. Parquet sets a temp staging Path for S3); every writer
106+ // thread then reuses this same instance read-only via SetConfig.
99107 adapter := newfunc ()
100- if err := utils .Unmarshal (config .WriterConfig , adapter .GetConfigRef ()); err != nil {
101- return nil , err
108+ parsedConfig := adapter .GetConfigRef ()
109+ if err := utils .Unmarshal (config .WriterConfig , parsedConfig ); err != nil {
110+ return nil , fmt .Errorf ("failed to unmarshal destination config: %s" , err )
102111 }
103112
104- err := adapter .Check (ctx )
105- if err != nil {
113+ // Start long-lived destination resources before any check/setup work.
114+ if in , ok := adapter .(Initializable ); ok {
115+ if err := in .Initialize (ctx ); err != nil {
116+ return nil , fmt .Errorf ("failed to initialize destination: %s" , err )
117+ }
118+ }
119+
120+ if err := adapter .Check (ctx ); err != nil {
121+ // Roll back anything Initialize started so we don't orphan it; there is
122+ // no pool object for the caller to Close on this error path.
123+ if s , ok := adapter .(Shutdownable ); ok {
124+ _ = s .Shutdown (ctx )
125+ }
106126 return nil , fmt .Errorf ("failed to test destination: %s" , err )
107127 }
108128
@@ -113,9 +133,10 @@ func NewWriterPool(ctx context.Context, config *types.WriterConfig, syncStreams
113133 ReadCount : atomic.Int64 {},
114134 RecordsFiltered : atomic.Int64 {},
115135 },
116- config : config .WriterConfig ,
117- init : newfunc ,
118- batchSize : batchSize ,
136+ lifecycle : adapter ,
137+ parsedConfig : parsedConfig ,
138+ init : newfunc ,
139+ batchSize : batchSize ,
119140 }
120141
121142 for _ , stream := range syncStreams {
@@ -128,6 +149,28 @@ func NewWriterPool(ctx context.Context, config *types.WriterConfig, syncStreams
128149 return pool , nil
129150}
130151
152+ // Close tears down destination-owned process resources (e.g. the Iceberg shared
153+ // JVM) via the pool's lifecycle adapter. Idempotent and safe to defer right
154+ // after a successful NewWriterPool.
155+ func (w * WriterPool ) Close (ctx context.Context ) {
156+ s , ok := w .lifecycle .(Shutdownable )
157+ if ! ok {
158+ return
159+ }
160+ if err := s .Shutdown (ctx ); err != nil {
161+ logger .Warnf ("WriterPool.Close: %s" , err )
162+ }
163+ }
164+
165+ // Clear drops the given streams from the destination, reusing the pool's
166+ // already-initialized lifecycle adapter (and its parsed config).
167+ func (w * WriterPool ) Clear (ctx context.Context , dropStreams []types.StreamInterface ) error {
168+ if len (dropStreams ) == 0 {
169+ return nil
170+ }
171+ return w .lifecycle .DropStreams (ctx , dropStreams )
172+ }
173+
131174func (w * WriterPool ) AddRecordsToSyncStats (count int64 ) {
132175 w .stats .TotalRecordsToSync .Add (count )
133176}
@@ -156,14 +199,10 @@ func (w *WriterPool) NewWriter(ctx context.Context, stream types.StreamInterface
156199
157200 var writerThread Writer
158201 prevStreamState , err := func () (* types.MetadataState , error ) {
159- // init writer with configurations
202+ // init writer and point it at the config parsed once at pool creation,
203+ // shared read-only across all writer threads.
160204 writerThread = w .init ()
161- w .configMutex .Lock ()
162- err := utils .Unmarshal (w .config , writerThread .GetConfigRef ())
163- w .configMutex .Unlock ()
164- if err != nil {
165- return nil , err
166- }
205+ writerThread .SetConfig (w .parsedConfig )
167206
168207 // setup table and schema
169208 streamArtifact .mu .Lock ()
@@ -296,68 +335,3 @@ func (wt *WriterThread) Close(ctx context.Context, finalMetadataState any) (err
296335 }
297336}
298337
299- func ClearDestination (ctx context.Context , config * types.WriterConfig , dropStreams []types.StreamInterface ) error {
300- newfunc , found := RegisteredWriters [config .Type ]
301- if ! found {
302- return fmt .Errorf ("invalid destination type has been passed [%s]" , config .Type )
303- }
304-
305- adapter := newfunc ()
306- if err := utils .Unmarshal (config .WriterConfig , adapter .GetConfigRef ()); err != nil {
307- return err
308- }
309-
310- if dropStreams != nil {
311- if err := adapter .DropStreams (ctx , dropStreams ); err != nil {
312- return fmt .Errorf ("failed to drop the streams: %s" , err )
313- }
314- }
315- return nil
316- }
317-
318- // Initialize starts destination-level process resources once, up front, if the
319- // registered writer implements Initializable. No-op for destinations without
320- // long-lived resources (parquet). Pair it with a deferred Shutdown.
321- func Initialize (ctx context.Context , config * types.WriterConfig ) error {
322- if config == nil {
323- return nil
324- }
325- newfunc , found := RegisteredWriters [config .Type ]
326- if ! found {
327- return fmt .Errorf ("invalid destination type has been passed [%s]" , config .Type )
328- }
329- adapter := newfunc ()
330- if err := utils .Unmarshal (config .WriterConfig , adapter .GetConfigRef ()); err != nil {
331- return fmt .Errorf ("failed to unmarshal destination config: %s" , err )
332- }
333- in , ok := adapter .(Initializable )
334- if ! ok {
335- return nil
336- }
337- return in .Initialize (ctx )
338- }
339-
340- // Shutdown invokes destination-level cleanup if the registered writer
341- // implements Shutdownable. No-op for destinations without long-lived
342- // resources (parquet).
343- func Shutdown (ctx context.Context , config * types.WriterConfig ) {
344- if config == nil {
345- return
346- }
347- newfunc , found := RegisteredWriters [config .Type ]
348- if ! found {
349- return
350- }
351- adapter := newfunc ()
352- if err := utils .Unmarshal (config .WriterConfig , adapter .GetConfigRef ()); err != nil {
353- logger .Warnf ("destination.Shutdown: failed to unmarshal config: %s" , err )
354- return
355- }
356- s , ok := adapter .(Shutdownable )
357- if ! ok {
358- return
359- }
360- if err := s .Shutdown (ctx ); err != nil {
361- logger .Warnf ("destination.Shutdown: %s" , err )
362- }
363- }
0 commit comments