001/*
002 * Copyright 2015-2022 Transmogrify LLC, 2022-2026 Revetware LLC.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.pyranid;
018
019import org.jspecify.annotations.NonNull;
020import org.jspecify.annotations.Nullable;
021
022import javax.annotation.concurrent.ThreadSafe;
023import java.time.Duration;
024import java.util.Objects;
025import java.util.Optional;
026import java.util.UUID;
027
028import static java.util.Objects.requireNonNull;
029
030/**
031 * Contract for collecting operational metrics from Pyranid.
032 * <p>
033 * The default collector is {@link #disabledInstance()}, which performs no work. {@link #inMemoryInstance()} returns a
034 * fresh counter-only collector useful for tests and ad-hoc inspection through {@link #snapshot()}.
035 * <p>
036 * Implementations must be thread-safe, non-blocking, and failure-tolerant. Pyranid catches and discards collector
037 * exceptions so metrics collection cannot affect database behavior.
038 *
039 * @author <a href="https://www.revetkn.com">Mark Allen</a>
040 * @since 4.2.0
041 */
042@ThreadSafe
043public interface MetricsCollector {
044        /**
045         * Called immediately before Pyranid attempts to acquire a statement-scoped {@link java.sql.Connection}.
046         * <p>
047         * Statement-scoped connections are used for standalone statement execution and stream opening outside a physical
048         * {@link Transaction}.
049         *
050         * @param ctx statement context for the operation that needs a connection
051         */
052        default void willAcquireStatementConnection(@NonNull StatementContext<?> ctx) {
053                // No-op by default
054        }
055
056        /**
057         * Called after Pyranid successfully acquires a statement-scoped {@link java.sql.Connection}.
058         *
059         * @param ctx                 statement context for the operation that acquired a connection
060         * @param acquisitionDuration elapsed time spent acquiring the connection
061         */
062        default void didAcquireStatementConnection(@NonNull StatementContext<?> ctx,
063                                                                                                                                                                                         @NonNull Duration acquisitionDuration) {
064                // No-op by default
065        }
066
067        /**
068         * Called after Pyranid fails to acquire a statement-scoped {@link java.sql.Connection}.
069         *
070         * @param ctx                 statement context for the operation that needed a connection
071         * @param databaseType        database type known at the time of failure
072         * @param acquisitionDuration elapsed time spent attempting to acquire the connection
073         * @param throwable           failure that prevented connection acquisition
074         */
075        default void didFailToAcquireStatementConnection(@NonNull StatementContext<?> ctx,
076                                                                                                                                                                                                                 @NonNull DatabaseType databaseType,
077                                                                                                                                                                                                                 @NonNull Duration acquisitionDuration,
078                                                                                                                                                                                                                 @NonNull Throwable throwable) {
079                // No-op by default
080        }
081
082        /**
083         * Called after Pyranid successfully releases a statement-scoped {@link java.sql.Connection}.
084         *
085         * @param ctx          statement context for the operation that used the connection
086         * @param heldDuration elapsed time between successful acquisition and release
087         */
088        default void didReleaseStatementConnection(@NonNull StatementContext<?> ctx,
089                                                                                                                                                                                         @NonNull Duration heldDuration) {
090                // No-op by default
091        }
092
093        /**
094         * Called after Pyranid fails to release a statement-scoped {@link java.sql.Connection}.
095         *
096         * @param ctx          statement context for the operation that used the connection
097         * @param heldDuration elapsed time between successful acquisition and the failed release attempt
098         * @param throwable    failure that prevented connection release
099         */
100        default void didFailToReleaseStatementConnection(@NonNull StatementContext<?> ctx,
101                                                                                                                                                                                                                 @NonNull Duration heldDuration,
102                                                                                                                                                                                                                 @NonNull Throwable throwable) {
103                // No-op by default
104        }
105
106        /**
107         * Called immediately before Pyranid attempts to acquire the JDBC connection backing a physical transaction.
108         * <p>
109         * Pyranid transactions acquire their JDBC connection lazily, so this callback is emitted only when transaction work
110         * first needs database access.
111         *
112         * @param transaction  transaction that needs a physical connection
113         * @param databaseType database type known at the time of acquisition
114         */
115        default void willAcquireTransactionConnection(@NonNull Transaction transaction,
116                                                                                                                                                                                                 @NonNull DatabaseType databaseType) {
117                // No-op by default
118        }
119
120        /**
121         * Called after Pyranid successfully acquires the JDBC connection backing a physical transaction.
122         *
123         * @param transaction         transaction that acquired the connection
124         * @param databaseType        database type known at the time of acquisition
125         * @param acquisitionDuration elapsed time spent acquiring the connection
126         */
127        default void didAcquireTransactionConnection(@NonNull Transaction transaction,
128                                                                                                                                                                                                @NonNull DatabaseType databaseType,
129                                                                                                                                                                                                @NonNull Duration acquisitionDuration) {
130                // No-op by default
131        }
132
133        /**
134         * Called after Pyranid fails to acquire the JDBC connection backing a physical transaction.
135         *
136         * @param transaction         transaction that needed a connection
137         * @param databaseType        database type known at the time of failure
138         * @param acquisitionDuration elapsed time spent attempting to acquire the connection
139         * @param throwable           failure that prevented connection acquisition
140         */
141        default void didFailToAcquireTransactionConnection(@NonNull Transaction transaction,
142                                                                                                                                                                                                                                @NonNull DatabaseType databaseType,
143                                                                                                                                                                                                                                @NonNull Duration acquisitionDuration,
144                                                                                                                                                                                                                                @NonNull Throwable throwable) {
145                // No-op by default
146        }
147
148        /**
149         * Called after Pyranid successfully releases the JDBC connection backing a physical transaction.
150         *
151         * @param transaction  transaction that owned the connection
152         * @param databaseType database type known at the time of release
153         * @param heldDuration elapsed time between successful acquisition and release
154         */
155        default void didReleaseTransactionConnection(@NonNull Transaction transaction,
156                                                                                                                                                                                                @NonNull DatabaseType databaseType,
157                                                                                                                                                                                                @NonNull Duration heldDuration) {
158                // No-op by default
159        }
160
161        /**
162         * Called after Pyranid fails to release the JDBC connection backing a physical transaction.
163         *
164         * @param transaction  transaction that owned the connection
165         * @param databaseType database type known at the time of failure
166         * @param heldDuration elapsed time between successful acquisition and the failed release attempt
167         * @param throwable    failure that prevented connection release
168         */
169        default void didFailToReleaseTransactionConnection(@NonNull Transaction transaction,
170                                                                                                                                                                                                                                @NonNull DatabaseType databaseType,
171                                                                                                                                                                                                                                @NonNull Duration heldDuration,
172                                                                                                                                                                                                                                @NonNull Throwable throwable) {
173                // No-op by default
174        }
175
176        /**
177         * Called when Pyranid enters a closure-based transaction.
178         * <p>
179         * This is a logical transaction event and may occur even if user code never performs database work and no physical JDBC
180         * transaction is begun.
181         *
182         * @param transaction  transaction passed to the closure through Pyranid APIs
183         * @param isolation    requested transaction isolation
184         * @param databaseType database type known at transaction entry
185         */
186        default void didEnterTransactionClosure(@NonNull Transaction transaction,
187                                                                                                                                                                                @NonNull TransactionIsolation isolation,
188                                                                                                                                                                                @NonNull DatabaseType databaseType) {
189                // No-op by default
190        }
191
192        /**
193         * Called when Pyranid exits a closure-based transaction.
194         *
195         * @param transaction     transaction that is exiting
196         * @param outcome         logical transaction outcome
197         * @param databaseType    database type known at transaction exit
198         * @param logicalDuration elapsed time from transaction entry to exit, including user code, physical transaction work,
199         *                        cleanup, and post-transaction operations
200         * @param thrown          exception or error that caused the transaction to exit abnormally, or {@code null} when the
201         *                        transaction completed without a reported failure
202         */
203        default void didExitTransactionClosure(@NonNull Transaction transaction,
204                                                                                                                                                                         @NonNull TransactionClosureOutcome outcome,
205                                                                                                                                                                         @NonNull DatabaseType databaseType,
206                                                                                                                                                                 @NonNull Duration logicalDuration,
207                                                                                                                                                                 @Nullable Throwable thrown) {
208                // No-op by default
209        }
210
211        /**
212         * Called after Pyranid successfully begins a physical JDBC transaction.
213         *
214         * @param transaction  transaction whose physical JDBC transaction began
215         * @param isolation    requested transaction isolation
216         * @param databaseType database type known at transaction begin
217         */
218        default void didBeginPhysicalTransaction(@NonNull Transaction transaction,
219                                                                                                                                                                                 @NonNull TransactionIsolation isolation,
220                                                                                                                                                                                 @NonNull DatabaseType databaseType) {
221                // No-op by default
222        }
223
224        /**
225         * Called after Pyranid fails while beginning a physical JDBC transaction.
226         *
227         * @param transaction  transaction whose physical JDBC transaction failed to begin
228         * @param isolation    requested transaction isolation
229         * @param phase        begin phase that failed
230         * @param databaseType database type known at the time of failure
231         * @param throwable    failure that prevented the physical transaction from beginning
232         */
233        default void didFailToBeginPhysicalTransaction(@NonNull Transaction transaction,
234                                                                                                                                                                                                        @NonNull TransactionIsolation isolation,
235                                                                                                                                                                                                        @NonNull PhysicalTransactionBeginFailurePhase phase,
236                                                                                                                                                                                                @NonNull DatabaseType databaseType,
237                                                                                                                                                                                                @NonNull Throwable throwable) {
238                // No-op by default
239        }
240
241        /**
242         * Called after Pyranid successfully commits a physical JDBC transaction.
243         *
244         * @param transaction      transaction that committed
245         * @param databaseType     database type known at commit time
246         * @param physicalDuration elapsed time between physical transaction begin and commit
247         */
248        default void didCommitPhysicalTransaction(@NonNull Transaction transaction,
249                                                                                                                                                                                        @NonNull DatabaseType databaseType,
250                                                                                                                                                                                        @NonNull Duration physicalDuration) {
251                // No-op by default
252        }
253
254        /**
255         * Called after Pyranid fails to commit a physical JDBC transaction.
256         *
257         * @param transaction      transaction whose commit failed
258         * @param databaseType     database type known at commit time
259         * @param physicalDuration elapsed time between physical transaction begin and the failed commit attempt
260         * @param throwable        failure that prevented commit from completing normally
261         */
262        default void didFailToCommitPhysicalTransaction(@NonNull Transaction transaction,
263                                                                                                                                                                                                         @NonNull DatabaseType databaseType,
264                                                                                                                                                                                                         @NonNull Duration physicalDuration,
265                                                                                                                                                                                                 @NonNull Throwable throwable) {
266                // No-op by default
267        }
268
269        /**
270         * Called after Pyranid successfully rolls back a physical JDBC transaction.
271         *
272         * @param transaction      transaction that rolled back
273         * @param databaseType     database type known at rollback time
274         * @param physicalDuration elapsed time between physical transaction begin and rollback
275         */
276        default void didRollbackPhysicalTransaction(@NonNull Transaction transaction,
277                                                                                                                                                                                         @NonNull DatabaseType databaseType,
278                                                                                                                                                                                         @NonNull Duration physicalDuration) {
279                // No-op by default
280        }
281
282        /**
283         * Called after Pyranid fails to roll back a physical JDBC transaction.
284         *
285         * @param transaction      transaction whose rollback failed
286         * @param databaseType     database type known at rollback time
287         * @param physicalDuration elapsed time between physical transaction begin and the failed rollback attempt
288         * @param throwable        failure that prevented rollback from completing normally
289         */
290        default void didFailToRollbackPhysicalTransaction(@NonNull Transaction transaction,
291                                                                                                                                                                                                                        @NonNull DatabaseType databaseType,
292                                                                                                                                                                                                                        @NonNull Duration physicalDuration,
293                                                                                                                                                                                                                @NonNull Throwable throwable) {
294                // No-op by default
295        }
296
297        /**
298         * Called after a post-transaction operation runs.
299         *
300         * @param transaction  transaction whose post-transaction operation ran
301         * @param result       transaction result visible to the post-transaction operation
302         * @param databaseType database type known at post-transaction execution time
303         * @param duration     elapsed time spent running the post-transaction operation
304         * @param throwable    failure thrown by the post-transaction operation, or {@code null} when it completed normally
305         */
306        default void didRunPostTransactionOperation(@NonNull Transaction transaction,
307                                                                                                                                                                                         @NonNull TransactionResult result,
308                                                                                                                                                                                         @NonNull DatabaseType databaseType,
309                                                                                                                                                                                 @NonNull Duration duration,
310                                                                                                                                                                                 @Nullable Throwable throwable) {
311                // No-op by default
312        }
313
314        /**
315         * Called after Pyranid successfully creates a transaction savepoint.
316         *
317         * @param transaction  transaction that created the savepoint
318         * @param databaseType database type known at savepoint creation time
319         */
320        default void didCreateSavepoint(@NonNull Transaction transaction,
321                                                                                                                                                @NonNull DatabaseType databaseType) {
322                // No-op by default
323        }
324
325        /**
326         * Called after Pyranid successfully rolls back to a transaction savepoint.
327         *
328         * @param transaction  transaction that rolled back to the savepoint
329         * @param databaseType database type known at savepoint rollback time
330         */
331        default void didRollbackToSavepoint(@NonNull Transaction transaction,
332                                                                                                                                                                @NonNull DatabaseType databaseType) {
333                // No-op by default
334        }
335
336        /**
337         * Called after Pyranid successfully releases a transaction savepoint.
338         *
339         * @param transaction  transaction that released the savepoint
340         * @param databaseType database type known at savepoint release time
341         */
342        default void didReleaseSavepoint(@NonNull Transaction transaction,
343                                                                                                                                                 @NonNull DatabaseType databaseType) {
344                // No-op by default
345        }
346
347        /**
348         * Called immediately before Pyranid executes a statement.
349         *
350         * @param ctx statement context for the statement about to execute
351         */
352        default void willExecuteStatement(@NonNull StatementContext<?> ctx) {
353                // No-op by default
354        }
355
356        /**
357         * Called after Pyranid successfully executes a statement.
358         *
359         * @param ctx          statement context for the executed statement
360         * @param statementLog diagnostic statement log for the execution
361         * @param result       statement execution result
362         */
363        default void didExecuteStatement(@NonNull StatementContext<?> ctx,
364                                                                                                                                                 @NonNull StatementLog<?> statementLog,
365                                                                                                                                                 @NonNull StatementResult result) {
366                // No-op by default
367        }
368
369        /**
370         * Called after Pyranid fails to execute a statement.
371         *
372         * @param ctx          statement context for the failed statement
373         * @param statementLog diagnostic statement log for the failed execution
374         * @param databaseType database type known at statement failure time
375         * @param throwable    failure thrown while executing the statement
376         */
377        default void didFailToExecuteStatement(@NonNull StatementContext<?> ctx,
378                                                                                                                                                                         @NonNull StatementLog<?> statementLog,
379                                                                                                                                                                         @NonNull DatabaseType databaseType,
380                                                                                                                                                                 @NonNull Throwable throwable) {
381                // No-op by default
382        }
383
384        /**
385         * Called immediately before Pyranid opens a streaming statement.
386         *
387         * @param ctx statement context for the stream about to open
388         */
389        default void willOpenStream(@NonNull StatementContext<?> ctx) {
390                // No-op by default
391        }
392
393        /**
394         * Called after Pyranid successfully opens a streaming statement.
395         *
396         * @param ctx          statement context for the opened stream
397         * @param openDuration elapsed time spent opening the stream
398         */
399        default void didOpenStream(@NonNull StatementContext<?> ctx,
400                                                                                                                         @NonNull Duration openDuration) {
401                // No-op by default
402        }
403
404        /**
405         * Called after Pyranid fails to open a streaming statement.
406         *
407         * @param ctx          statement context for the stream that failed to open
408         * @param databaseType database type known at stream-open failure time
409         * @param openDuration elapsed time spent attempting to open the stream
410         * @param throwable    failure that prevented the stream from opening
411         */
412        default void didFailToOpenStream(@NonNull StatementContext<?> ctx,
413                                                                                                                                                 @NonNull DatabaseType databaseType,
414                                                                                                                                                 @NonNull Duration openDuration,
415                                                                                                                                                 @NonNull Throwable throwable) {
416                // No-op by default
417        }
418
419        /**
420         * Called after an opened stream reaches a terminal state.
421         * <p>
422         * Pyranid does not emit this callback for streams that fail before they open; those failures are reported through
423         * {@link #didFailToOpenStream(StatementContext, DatabaseType, Duration, Throwable)}.
424         *
425         * @param ctx            statement context for the stream
426         * @param outcome        terminal stream outcome
427         * @param rowsConsumed   number of rows consumed from the stream
428         * @param streamDuration elapsed time between stream-open start and terminal close handling
429         * @param throwable      failure associated with the terminal outcome, or {@code null} when no failure is available
430         */
431        default void didCloseStream(@NonNull StatementContext<?> ctx,
432                                                                                                                        @NonNull StreamTerminalOutcome outcome,
433                                                                                                                        @NonNull Long rowsConsumed,
434                                                                                                                        @NonNull Duration streamDuration,
435                                                                                                                        @Nullable Throwable throwable) {
436                // No-op by default
437        }
438
439        /**
440         * Called immediately before Pyranid attempts to open a notification session.
441         * <p>
442         * Argument validation, ambient-transaction rejection, database-type resolution, and backend-specific channel
443         * validation occur before this callback. Exactly one of
444         * {@link #didOpenNotificationSession(DatabaseType, UUID, Duration)} or
445         * {@link #didFailToOpenNotificationSession(DatabaseType, UUID, Duration, Throwable)} follows it.
446         *
447         * @param databaseType         database type resolved for the notification session
448         * @param notificationSessionId identifier for this notification-session invocation
449         * @since 4.6.0
450         */
451        default void willOpenNotificationSession(@NonNull DatabaseType databaseType,
452                                                                                                                                                                        @NonNull UUID notificationSessionId) {
453                // No-op by default
454        }
455
456        /**
457         * Called after Pyranid successfully opens a notification session and registers every requested channel.
458         * <p>
459         * This callback occurs before the application operation is dispatched. It does not indicate that application
460         * reconciliation or readiness work has completed.
461         *
462         * @param databaseType          database type resolved for the notification session
463         * @param notificationSessionId identifier for this notification-session invocation
464         * @param openDuration          elapsed time from the corresponding
465         *                              {@link #willOpenNotificationSession(DatabaseType, UUID)} callback through successful
466         *                              channel registration
467         * @since 4.6.0
468         */
469        default void didOpenNotificationSession(@NonNull DatabaseType databaseType,
470                                                                                                                                                                 @NonNull UUID notificationSessionId,
471                                                                                                                                                                 @NonNull Duration openDuration) {
472                // No-op by default
473        }
474
475        /**
476         * Called after Pyranid fails to open a notification session.
477         * <p>
478         * This is the terminal lifecycle callback for an invocation that never reached
479         * {@link #didOpenNotificationSession(DatabaseType, UUID, Duration)}. It is not followed by
480         * {@link #didCloseNotificationSession(DatabaseType, UUID, NotificationSessionOutcome, Duration, Throwable)}.
481         * Pyranid emits it after selecting the opening failure and before cleaning up any acquired candidate connection.
482         *
483         * @param databaseType          database type resolved for the notification session
484         * @param notificationSessionId identifier for this notification-session invocation
485         * @param openDuration          elapsed time from the corresponding
486         *                              {@link #willOpenNotificationSession(DatabaseType, UUID)} callback through the opening
487         *                              failure
488         * @param throwable             failure that prevented the notification session from opening
489         * @since 4.6.0
490         */
491        default void didFailToOpenNotificationSession(@NonNull DatabaseType databaseType,
492                                                                                                                                                                         @NonNull UUID notificationSessionId,
493                                                                                                                                                                         @NonNull Duration openDuration,
494                                                                                                                                                                         @NonNull Throwable throwable) {
495                // No-op by default
496        }
497
498        /**
499         * Called when Pyranid delivers a nonempty notification batch to application code.
500         * <p>
501         * Empty receive results and notifications discarded during setup or cleanup are not reported.
502         *
503         * @param databaseType          database type resolved for the notification session
504         * @param notificationSessionId identifier for the notification session that delivered the batch
505         * @param notificationCount     number of notifications in the delivered batch
506         * @since 4.6.0
507         */
508        default void didDeliverNotificationBatch(@NonNull DatabaseType databaseType,
509                                                                                                                                                                        @NonNull UUID notificationSessionId,
510                                                                                                                                                                        @NonNull Long notificationCount) {
511                // No-op by default
512        }
513
514        /**
515         * Called when a receive operation detects terminal uncertainty or loss of an opened notification connection.
516         * <p>
517         * This callback is emitted at most once for a notification session. It does not report setup failures, failures
518         * from ordinary database operations performed by application code, or cleanup-only failures.
519         *
520         * @param databaseType          database type resolved for the notification session
521         * @param notificationSessionId identifier for the notification session whose connection was lost
522         * @param throwable             terminal connection failure
523         * @since 4.6.0
524         */
525        default void didLoseNotificationConnection(@NonNull DatabaseType databaseType,
526                                                                                                                                                                                @NonNull UUID notificationSessionId,
527                                                                                                                                                                                @NonNull Throwable throwable) {
528                // No-op by default
529        }
530
531        /**
532         * Called after an opened notification session reaches a terminal state and cleanup completes.
533         * <p>
534         * Every invocation that emits {@link #didOpenNotificationSession(DatabaseType, UUID, Duration)} emits this callback
535         * exactly once. Invocations that fail before opening terminate through
536         * {@link #didFailToOpenNotificationSession(DatabaseType, UUID, Duration, Throwable)} instead.
537         *
538         * @param databaseType          database type resolved for the notification session
539         * @param notificationSessionId identifier for the notification session
540         * @param outcome               terminal notification-session outcome
541         * @param sessionDuration       elapsed time from the corresponding
542         *                              {@link #willOpenNotificationSession(DatabaseType, UUID)} callback through completed
543         *                              cleanup
544         * @param throwable             selected failure for {@link NotificationSessionOutcome#FAILED}, or {@code null} for
545         *                              {@link NotificationSessionOutcome#CALLBACK_RETURNED} and
546         *                              {@link NotificationSessionOutcome#INTERRUPTED}
547         * @since 4.6.0
548         */
549        default void didCloseNotificationSession(@NonNull DatabaseType databaseType,
550                                                                                                                                                                         @NonNull UUID notificationSessionId,
551                                                                                                                                                                         @NonNull NotificationSessionOutcome outcome,
552                                                                                                                                                                         @NonNull Duration sessionDuration,
553                                                                                                                                                                         @Nullable Throwable throwable) {
554                // No-op by default
555        }
556
557        /**
558         * Returns a notification counter snapshot if this collector supports in-process inspection.
559         * <p>
560         * Implementations may read counters independently without cross-counter atomicity. Callers that need
561         * invariant-consistent snapshots should drain in-flight work before reading.
562         *
563         * @return a notification metrics snapshot, if supported
564         * @since 4.6.0
565         */
566        @NonNull
567        default Optional<NotificationSnapshot> notificationSnapshot() {
568                return Optional.empty();
569        }
570
571        /**
572         * Returns a counter snapshot if this collector supports in-process inspection.
573         * <p>
574         * Implementations may read counters independently without cross-counter atomicity. Callers that need invariant-consistent
575         * snapshots should drain in-flight work before reading.
576         *
577         * @return a metrics snapshot, if supported
578         */
579        @NonNull
580        default Optional<Snapshot> snapshot() {
581                return Optional.empty();
582        }
583
584        /**
585         * Best-effort reset for test and ad-hoc use.
586         * <p>
587         * Concurrent updates may race with reset operations. Production rolling-window semantics should be implemented outside
588         * this interface.
589         */
590        default void reset() {
591                // No-op by default
592        }
593
594        /**
595         * Returns the shared no-op metrics collector.
596         *
597         * @return no-op metrics collector used when metrics collection is disabled
598         */
599        @NonNull
600        static MetricsCollector disabledInstance() {
601                return DisabledMetricsCollector.defaultInstance();
602        }
603
604        /**
605         * Creates a fresh in-memory counter collector.
606         * <p>
607         * This collector is intended for tests and lightweight local inspection through {@link #snapshot()} and
608         * {@link #notificationSnapshot()}.
609         *
610         * @return new in-memory metrics collector
611         */
612        @NonNull
613        static MetricsCollector inMemoryInstance() {
614                return InMemoryMetricsCollector.defaultInstance();
615        }
616
617        /**
618         * Terminal outcome for an opened notification session.
619         *
620         * @since 4.6.0
621         */
622        enum NotificationSessionOutcome {
623                /**
624                 * The application callback returned and notification-session cleanup completed normally.
625                 */
626                CALLBACK_RETURNED,
627
628                /**
629                 * Cooperative interruption ended the notification session and cleanup completed normally.
630                 */
631                INTERRUPTED,
632
633                /**
634                 * Setup after opening, notification receive, application callback, or cleanup failed.
635                 */
636                FAILED
637        }
638
639        /**
640         * Counter snapshot for notification metrics.
641         * <p>
642         * Implementations may read counters independently without cross-counter atomicity. Callers that need
643         * invariant-consistent snapshots should drain in-flight work before reading.
644         * <p>
645         * Instances are immutable value objects created through {@link #of(Long, Long, Long, Long, Long, Long, Long)}.
646         * Future releases may add counters and accessors while retaining this factory with defined defaults for any
647         * newly added values.
648         *
649         * @since 4.6.0
650         */
651        @ThreadSafe
652        final class NotificationSnapshot {
653                @NonNull
654                private final Long sessionsStarted;
655                @NonNull
656                private final Long sessionsOpened;
657                @NonNull
658                private final Long sessionsCallbackReturned;
659                @NonNull
660                private final Long sessionsInterrupted;
661                @NonNull
662                private final Long sessionsFailed;
663                @NonNull
664                private final Long batchesDelivered;
665                @NonNull
666                private final Long notificationsDelivered;
667
668                private NotificationSnapshot(@NonNull Long sessionsStarted,
669                                                                                                                 @NonNull Long sessionsOpened,
670                                                                                                                 @NonNull Long sessionsCallbackReturned,
671                                                                                                                 @NonNull Long sessionsInterrupted,
672                                                                                                                 @NonNull Long sessionsFailed,
673                                                                                                                 @NonNull Long batchesDelivered,
674                                                                                                                 @NonNull Long notificationsDelivered) {
675                        this.sessionsStarted = requireNonNull(sessionsStarted);
676                        this.sessionsOpened = requireNonNull(sessionsOpened);
677                        this.sessionsCallbackReturned = requireNonNull(sessionsCallbackReturned);
678                        this.sessionsInterrupted = requireNonNull(sessionsInterrupted);
679                        this.sessionsFailed = requireNonNull(sessionsFailed);
680                        this.batchesDelivered = requireNonNull(batchesDelivered);
681                        this.notificationsDelivered = requireNonNull(notificationsDelivered);
682                }
683
684                /**
685                 * Creates a notification metrics snapshot.
686                 *
687                 * @param sessionsStarted          valid, type-resolved notification-session invocations that entered measured setup
688                 * @param sessionsOpened           notification sessions that completed channel registration
689                 * @param sessionsCallbackReturned opened notification sessions whose callbacks returned and cleanup completed normally
690                 * @param sessionsInterrupted      opened notification sessions that ended through clean cooperative interruption
691                 * @param sessionsFailed           notification-session invocations that failed before opening or after opening
692                 * @param batchesDelivered         nonempty notification batches delivered to application code
693                 * @param notificationsDelivered   notifications contained in delivered batches
694                 * @return notification metrics snapshot
695                 * @throws NullPointerException if any counter is null
696                 * @since 4.6.0
697                 */
698                @NonNull
699                public static NotificationSnapshot of(@NonNull Long sessionsStarted,
700                                                                                                                                                                                         @NonNull Long sessionsOpened,
701                                                                                                                                                                                         @NonNull Long sessionsCallbackReturned,
702                                                                                                                                                                                         @NonNull Long sessionsInterrupted,
703                                                                                                                                                                                         @NonNull Long sessionsFailed,
704                                                                                                                                                                                         @NonNull Long batchesDelivered,
705                                                                                                                                                                                         @NonNull Long notificationsDelivered) {
706                        return new NotificationSnapshot(sessionsStarted, sessionsOpened, sessionsCallbackReturned, sessionsInterrupted,
707                                        sessionsFailed, batchesDelivered, notificationsDelivered);
708                }
709
710                /**
711                 * @return valid, type-resolved notification-session invocations that entered measured setup
712                 * @since 4.6.0
713                 */
714                @NonNull
715                public Long sessionsStarted() {
716                        return this.sessionsStarted;
717                }
718
719                /**
720                 * @return notification sessions that completed channel registration
721                 * @since 4.6.0
722                 */
723                @NonNull
724                public Long sessionsOpened() {
725                        return this.sessionsOpened;
726                }
727
728                /**
729                 * @return opened notification sessions whose callbacks returned and cleanup completed normally
730                 * @since 4.6.0
731                 */
732                @NonNull
733                public Long sessionsCallbackReturned() {
734                        return this.sessionsCallbackReturned;
735                }
736
737                /**
738                 * @return opened notification sessions that ended through clean cooperative interruption
739                 * @since 4.6.0
740                 */
741                @NonNull
742                public Long sessionsInterrupted() {
743                        return this.sessionsInterrupted;
744                }
745
746                /**
747                 * @return notification-session invocations that failed before opening or after opening
748                 * @since 4.6.0
749                 */
750                @NonNull
751                public Long sessionsFailed() {
752                        return this.sessionsFailed;
753                }
754
755                /**
756                 * @return nonempty notification batches delivered to application code
757                 * @since 4.6.0
758                 */
759                @NonNull
760                public Long batchesDelivered() {
761                        return this.batchesDelivered;
762                }
763
764                /**
765                 * @return notifications contained in delivered batches
766                 * @since 4.6.0
767                 */
768                @NonNull
769                public Long notificationsDelivered() {
770                        return this.notificationsDelivered;
771                }
772
773                @Override
774                public boolean equals(@Nullable Object object) {
775                        if (this == object)
776                                return true;
777
778                        if (!(object instanceof NotificationSnapshot notificationSnapshot))
779                                return false;
780
781                        return sessionsStarted().equals(notificationSnapshot.sessionsStarted())
782                                        && sessionsOpened().equals(notificationSnapshot.sessionsOpened())
783                                        && sessionsCallbackReturned().equals(notificationSnapshot.sessionsCallbackReturned())
784                                        && sessionsInterrupted().equals(notificationSnapshot.sessionsInterrupted())
785                                        && sessionsFailed().equals(notificationSnapshot.sessionsFailed())
786                                        && batchesDelivered().equals(notificationSnapshot.batchesDelivered())
787                                        && notificationsDelivered().equals(notificationSnapshot.notificationsDelivered());
788                }
789
790                @Override
791                public int hashCode() {
792                        return Objects.hash(sessionsStarted(), sessionsOpened(), sessionsCallbackReturned(), sessionsInterrupted(),
793                                        sessionsFailed(), batchesDelivered(), notificationsDelivered());
794                }
795
796                @Override
797                @NonNull
798                public String toString() {
799                        return "NotificationSnapshot[" +
800                                        "sessionsStarted=" + sessionsStarted() +
801                                        ", sessionsOpened=" + sessionsOpened() +
802                                        ", sessionsCallbackReturned=" + sessionsCallbackReturned() +
803                                        ", sessionsInterrupted=" + sessionsInterrupted() +
804                                        ", sessionsFailed=" + sessionsFailed() +
805                                        ", batchesDelivered=" + batchesDelivered() +
806                                        ", notificationsDelivered=" + notificationsDelivered() +
807                                        ']';
808                }
809        }
810
811        /**
812         * Logical outcome for a closure-based transaction.
813         */
814        enum TransactionClosureOutcome {
815                /**
816                 * The transaction used a physical JDBC transaction and commit completed normally.
817                 */
818                COMMITTED,
819
820                /**
821                 * The transaction used a physical JDBC transaction and was rolled back, including a recognized commit-time
822                 * serialization failure whose follow-up rollback completed normally.
823                 */
824                ROLLED_BACK,
825
826                /**
827                 * The transaction closure exited without ever acquiring a JDBC connection.
828                 */
829                NO_PHYSICAL_TX,
830
831                /**
832                 * The transaction failed before Pyranid could report a normal commit or rollback outcome.
833                 */
834                FAILED
835        }
836
837        /**
838         * Physical transaction begin phase that failed.
839         */
840        enum PhysicalTransactionBeginFailurePhase {
841                /**
842                 * Connection acquisition from the configured {@link javax.sql.DataSource} failed.
843                 */
844                ACQUIRE_CONNECTION,
845
846                /**
847                 * Reading the connection's initial autocommit setting failed.
848                 */
849                READ_INITIAL_AUTOCOMMIT,
850
851                /**
852                 * Reading the connection's initial transaction isolation failed.
853                 */
854                READ_INITIAL_ISOLATION,
855
856                /**
857                 * Reading the connection's initial read-only setting failed.
858                 */
859                READ_INITIAL_READ_ONLY,
860
861                /**
862                 * Disabling autocommit for the transaction failed.
863                 */
864                SET_AUTOCOMMIT_FALSE,
865
866                /**
867                 * Applying the requested transaction isolation failed.
868                 */
869                SET_ISOLATION,
870
871                /**
872                 * Applying the requested read-only setting failed.
873                 */
874                SET_READ_ONLY
875        }
876
877        /**
878         * Terminal outcome for an opened stream.
879         */
880        enum StreamTerminalOutcome {
881                /**
882                 * Stream iteration reached the end of the result set and cleanup completed without an iteration or callback failure.
883                 */
884                COMPLETED_NORMALLY,
885
886                /**
887                 * The stream was closed before all rows were consumed.
888                 */
889                EARLY_CLOSE,
890
891                /**
892                 * The caller-provided stream callback failed.
893                 */
894                CALLBACK_FAILURE,
895
896                /**
897                 * Result-set iteration failed while the stream was open.
898                 */
899                ITERATION_FAILURE,
900
901                /**
902                 * The stream failed before it opened.
903                 * <p>
904                 * Pyranid normally reports this through {@link #didFailToOpenStream(StatementContext, DatabaseType, Duration,
905                 * Throwable)} instead of {@link #didCloseStream(StatementContext, StreamTerminalOutcome, Long, Duration, Throwable)}.
906                 */
907                OPEN_FAILURE
908        }
909
910        /**
911         * Counter snapshot for collectors that support in-process inspection.
912         * <p>
913         * Implementations may read counters independently without cross-counter atomicity. Callers that need invariant-consistent
914         * snapshots should drain in-flight work before reading.
915         *
916         * @param connectionsAcquiredStatementScope          statement-scoped connection acquisitions that completed normally
917         * @param connectionsAcquiredTransactionScope        transaction-scoped connection acquisitions that completed normally
918         * @param connectionsFailedStatementScope            statement-scoped connection acquisitions that failed
919         * @param connectionsFailedTransactionScope          transaction-scoped connection acquisitions that failed
920         * @param connectionReleaseFailuresStatementScope    statement-scoped connection releases that failed
921         * @param connectionReleaseFailuresTransactionScope  transaction-scoped connection releases that failed
922         * @param transactionClosuresEntered                 closure-based transactions entered
923         * @param transactionClosuresExited                  closure-based transactions exited
924         * @param transactionClosuresCommitted               closure-based transactions that committed
925         * @param transactionClosuresRolledBack              closure-based transactions that rolled back
926         * @param transactionClosuresNoPhysical              closure-based transactions that exited without a physical JDBC transaction
927         * @param transactionClosuresFailed                  closure-based transactions that failed before a normal commit or rollback outcome
928         * @param physicalTransactionsBegun                  physical JDBC transactions begun
929         * @param physicalTransactionsBeginFailed            physical JDBC transactions that failed while beginning
930         * @param physicalTransactionsCommitted              physical JDBC transactions committed
931         * @param physicalTransactionsCommitFailed           physical JDBC transactions whose commit failed
932         * @param physicalTransactionsRolledBack             physical JDBC transactions rolled back
933         * @param physicalTransactionsRollbackFailed         physical JDBC transactions whose rollback failed
934         * @param savepointsCreated                          transaction savepoints created
935         * @param savepointsRolledBack                       transaction savepoints rolled back to
936         * @param savepointsReleased                         transaction savepoints released
937         * @param statementsExecuted                         statements that executed successfully
938         * @param statementsFailed                           statements that failed during execution
939         * @param streamsOpened                              streams that opened successfully
940         * @param streamsOpenFailures                        streams that failed before opening
941         * @param streamsClosedNormally                      opened streams that reached the end of the result set
942         * @param streamsEarlyClosed                         opened streams that closed before all rows were consumed
943         * @param streamsCallbackFailed                      opened streams whose caller-provided callback failed
944         * @param streamsIterationFailed                     opened streams whose result-set iteration failed
945         * @param postTransactionOperationsRun               post-transaction operations that ran
946         * @param postTransactionOperationsFailed            post-transaction operations that failed
947         */
948        @ThreadSafe
949        record Snapshot(@NonNull Long connectionsAcquiredStatementScope,
950                                                                        @NonNull Long connectionsAcquiredTransactionScope,
951                                                                        @NonNull Long connectionsFailedStatementScope,
952                                                                        @NonNull Long connectionsFailedTransactionScope,
953                                                                        @NonNull Long connectionReleaseFailuresStatementScope,
954                                                                        @NonNull Long connectionReleaseFailuresTransactionScope,
955                                                                        @NonNull Long transactionClosuresEntered,
956                                                                        @NonNull Long transactionClosuresExited,
957                                                                        @NonNull Long transactionClosuresCommitted,
958                                                                        @NonNull Long transactionClosuresRolledBack,
959                                                                        @NonNull Long transactionClosuresNoPhysical,
960                                                                        @NonNull Long transactionClosuresFailed,
961                                                                        @NonNull Long physicalTransactionsBegun,
962                                                                        @NonNull Long physicalTransactionsBeginFailed,
963                                                                        @NonNull Long physicalTransactionsCommitted,
964                                                                        @NonNull Long physicalTransactionsCommitFailed,
965                                                                        @NonNull Long physicalTransactionsRolledBack,
966                                                                        @NonNull Long physicalTransactionsRollbackFailed,
967                                                                        @NonNull Long savepointsCreated,
968                                                                        @NonNull Long savepointsRolledBack,
969                                                                        @NonNull Long savepointsReleased,
970                                                                        @NonNull Long statementsExecuted,
971                                                                        @NonNull Long statementsFailed,
972                                                                        @NonNull Long streamsOpened,
973                                                                        @NonNull Long streamsOpenFailures,
974                                                                        @NonNull Long streamsClosedNormally,
975                                                                        @NonNull Long streamsEarlyClosed,
976                                                                        @NonNull Long streamsCallbackFailed,
977                                                                        @NonNull Long streamsIterationFailed,
978                                                                        @NonNull Long postTransactionOperationsRun,
979                                                                        @NonNull Long postTransactionOperationsFailed) {
980        }
981}