@@ -82,18 +82,9 @@ func (d *datastorePersist[K, V]) Load(ctx context.Context, key K) (V, time.Time,
8282 return zero , time.Time {}, false , fmt .Errorf ("datastore get: %w" , err )
8383 }
8484
85- // Check expiration
85+ // Check expiration - return miss but don't delete
86+ // Cleanup is handled by native Datastore TTL or periodic Cleanup() calls
8687 if ! entry .Expiry .IsZero () && time .Now ().After (entry .Expiry ) {
87- // Delete expired entry asynchronously with timeout
88- // Note: Using context.Background() intentionally - cleanup should continue even if parent context is cancelled
89- go func (_ context.Context ) {
90- //nolint:contextcheck // Using Background intentionally for independent cleanup goroutine
91- cleanupCtx , cancel := context .WithTimeout (context .Background (), 5 * time .Second )
92- defer cancel ()
93- if err := d .client .Delete (cleanupCtx , dsKey ); err != nil {
94- slog .Debug ("failed to delete expired entry" , "error" , err )
95- }
96- }(ctx )
9788 return zero , time.Time {}, false , nil
9889 }
9990
@@ -187,17 +178,8 @@ func (d *datastorePersist[K, V]) LoadRecent(ctx context.Context, limit int) (<-c
187178 default :
188179 }
189180
190- // Clean up expired entries asynchronously with timeout
181+ // Skip expired entries - cleanup is handled by native TTL or periodic Cleanup() calls
191182 if ! entry .Expiry .IsZero () && now .After (entry .Expiry ) {
192- // Note: Using context.Background() intentionally - cleanup should continue even if parent context is cancelled
193- go func (_ context.Context , key * datastore.Key ) {
194- //nolint:contextcheck // Using Background intentionally for independent cleanup goroutine
195- cleanupCtx , cancel := context .WithTimeout (context .Background (), 5 * time .Second )
196- defer cancel ()
197- if err := d .client .Delete (cleanupCtx , key ); err != nil {
198- slog .Debug ("failed to delete expired entry" , "error" , err )
199- }
200- }(ctx , dsKey )
201183 expired ++
202184 continue
203185 }
@@ -250,6 +232,37 @@ func (d *datastorePersist[K, V]) LoadAll(ctx context.Context) (<-chan Entry[K, V
250232 return d .LoadRecent (ctx , 0 )
251233}
252234
235+ // Cleanup removes expired entries from Datastore.
236+ // maxAge specifies how old entries must be (based on expiry field) before deletion.
237+ // If native Datastore TTL is properly configured, this will find no entries.
238+ func (d * datastorePersist [K , V ]) Cleanup (ctx context.Context , maxAge time.Duration ) (int , error ) {
239+ cutoff := time .Now ().Add (- maxAge )
240+
241+ // Query for entries with expiry before cutoff
242+ // This finds entries that should have expired based on maxAge
243+ query := datastore .NewQuery (d .kind ).
244+ Filter ("expiry >" , time.Time {}).
245+ Filter ("expiry <" , cutoff ).
246+ KeysOnly ()
247+
248+ keys , err := d .client .GetAll (ctx , query , nil )
249+ if err != nil {
250+ return 0 , fmt .Errorf ("query expired entries: %w" , err )
251+ }
252+
253+ if len (keys ) == 0 {
254+ return 0 , nil
255+ }
256+
257+ // Batch delete expired entries
258+ if err := d .client .DeleteMulti (ctx , keys ); err != nil {
259+ return 0 , fmt .Errorf ("delete expired entries: %w" , err )
260+ }
261+
262+ slog .Info ("cleaned up expired entries" , "count" , len (keys ), "kind" , d .kind )
263+ return len (keys ), nil
264+ }
265+
253266// Close releases Datastore client resources.
254267func (d * datastorePersist [K , V ]) Close () error {
255268 return d .client .Close ()
0 commit comments