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         * <p>
257         * Reported when Pyranid's physical commit attempt fails: the JDBC commit call itself, or the success handling that
258         * immediately follows it. It does not establish that the driver never received the commit, nor that the commit did not
259         * succeed. A completion that refuses the commit before issuing it, such as a savepoint-scope ownership conflict, still
260         * fails the transaction without reaching this callback.
261         *
262         * @param transaction      transaction whose commit failed
263         * @param databaseType     database type known at commit time
264         * @param physicalDuration elapsed time between physical transaction begin and the failed commit attempt
265         * @param throwable        failure that prevented commit from completing normally
266         */
267        default void didFailToCommitPhysicalTransaction(@NonNull Transaction transaction,
268                                                                                                                                                                                                         @NonNull DatabaseType databaseType,
269                                                                                                                                                                                                         @NonNull Duration physicalDuration,
270                                                                                                                                                                                                 @NonNull Throwable throwable) {
271                // No-op by default
272        }
273
274        /**
275         * Called after Pyranid successfully rolls back a physical JDBC transaction.
276         *
277         * @param transaction      transaction that rolled back
278         * @param databaseType     database type known at rollback time
279         * @param physicalDuration elapsed time between physical transaction begin and rollback
280         */
281        default void didRollbackPhysicalTransaction(@NonNull Transaction transaction,
282                                                                                                                                                                                         @NonNull DatabaseType databaseType,
283                                                                                                                                                                                         @NonNull Duration physicalDuration) {
284                // No-op by default
285        }
286
287        /**
288         * Called after Pyranid fails to roll back a physical JDBC transaction.
289         *
290         * @param transaction      transaction whose rollback failed
291         * @param databaseType     database type known at rollback time
292         * @param physicalDuration elapsed time between physical transaction begin and the failed rollback attempt
293         * @param throwable        failure that prevented rollback from completing normally
294         */
295        default void didFailToRollbackPhysicalTransaction(@NonNull Transaction transaction,
296                                                                                                                                                                                                                        @NonNull DatabaseType databaseType,
297                                                                                                                                                                                                                        @NonNull Duration physicalDuration,
298                                                                                                                                                                                                                @NonNull Throwable throwable) {
299                // No-op by default
300        }
301
302        /**
303         * Called after a post-transaction operation runs.
304         *
305         * @param transaction  transaction whose post-transaction operation ran
306         * @param result       transaction result visible to the post-transaction operation
307         * @param databaseType database type known at post-transaction execution time
308         * @param duration     elapsed time spent running the post-transaction operation
309         * @param throwable    failure thrown by the post-transaction operation, or {@code null} when it completed normally
310         */
311        default void didRunPostTransactionOperation(@NonNull Transaction transaction,
312                                                                                                                                                                                         @NonNull TransactionResult result,
313                                                                                                                                                                                         @NonNull DatabaseType databaseType,
314                                                                                                                                                                                 @NonNull Duration duration,
315                                                                                                                                                                                 @Nullable Throwable throwable) {
316                // No-op by default
317        }
318
319        /**
320         * Called after Pyranid successfully creates a transaction savepoint.
321         *
322         * @param transaction  transaction that created the savepoint
323         * @param databaseType database type known at savepoint creation time
324         */
325        default void didCreateSavepoint(@NonNull Transaction transaction,
326                                                                                                                                                @NonNull DatabaseType databaseType) {
327                // No-op by default
328        }
329
330        /**
331         * Called after Pyranid successfully rolls back to a transaction savepoint.
332         *
333         * @param transaction  transaction that rolled back to the savepoint
334         * @param databaseType database type known at savepoint rollback time
335         */
336        default void didRollbackToSavepoint(@NonNull Transaction transaction,
337                                                                                                                                                                @NonNull DatabaseType databaseType) {
338                // No-op by default
339        }
340
341        /**
342         * Called after Pyranid successfully releases a transaction savepoint.
343         *
344         * @param transaction  transaction that released the savepoint
345         * @param databaseType database type known at savepoint release time
346         */
347        default void didReleaseSavepoint(@NonNull Transaction transaction,
348                                                                                                                                                 @NonNull DatabaseType databaseType) {
349                // No-op by default
350        }
351
352        /**
353         * Called immediately before Pyranid executes a statement.
354         *
355         * @param ctx statement context for the statement about to execute
356         */
357        default void willExecuteStatement(@NonNull StatementContext<?> ctx) {
358                // No-op by default
359        }
360
361        /**
362         * Called after Pyranid successfully executes a statement.
363         *
364         * @param ctx          statement context for the executed statement
365         * @param statementLog diagnostic statement log for the execution
366         * @param result       statement execution result
367         */
368        default void didExecuteStatement(@NonNull StatementContext<?> ctx,
369                                                                                                                                                 @NonNull StatementLog<?> statementLog,
370                                                                                                                                                 @NonNull StatementResult result) {
371                // No-op by default
372        }
373
374        /**
375         * Called after Pyranid fails to execute a statement.
376         *
377         * @param ctx          statement context for the failed statement
378         * @param statementLog diagnostic statement log for the failed execution
379         * @param databaseType database type known at statement failure time
380         * @param throwable    failure thrown while executing the statement
381         */
382        default void didFailToExecuteStatement(@NonNull StatementContext<?> ctx,
383                                                                                                                                                                         @NonNull StatementLog<?> statementLog,
384                                                                                                                                                                         @NonNull DatabaseType databaseType,
385                                                                                                                                                                 @NonNull Throwable throwable) {
386                // No-op by default
387        }
388
389        /**
390         * Called immediately before Pyranid opens a streaming statement.
391         *
392         * @param ctx statement context for the stream about to open
393         */
394        default void willOpenStream(@NonNull StatementContext<?> ctx) {
395                // No-op by default
396        }
397
398        /**
399         * Called after Pyranid successfully opens a streaming statement.
400         *
401         * @param ctx          statement context for the opened stream
402         * @param openDuration elapsed time spent opening the stream
403         */
404        default void didOpenStream(@NonNull StatementContext<?> ctx,
405                                                                                                                         @NonNull Duration openDuration) {
406                // No-op by default
407        }
408
409        /**
410         * Called after Pyranid fails to open a streaming statement.
411         *
412         * @param ctx          statement context for the stream that failed to open
413         * @param databaseType database type known at stream-open failure time
414         * @param openDuration elapsed time spent attempting to open the stream
415         * @param throwable    failure that prevented the stream from opening
416         */
417        default void didFailToOpenStream(@NonNull StatementContext<?> ctx,
418                                                                                                                                                 @NonNull DatabaseType databaseType,
419                                                                                                                                                 @NonNull Duration openDuration,
420                                                                                                                                                 @NonNull Throwable throwable) {
421                // No-op by default
422        }
423
424        /**
425         * Called after an opened stream reaches a terminal state.
426         * <p>
427         * Pyranid does not emit this callback for streams that fail before they open; those failures are reported through
428         * {@link #didFailToOpenStream(StatementContext, DatabaseType, Duration, Throwable)}.
429         *
430         * @param ctx            statement context for the stream
431         * @param outcome        terminal stream outcome
432         * @param rowsConsumed   number of rows consumed from the stream
433         * @param streamDuration elapsed time between stream-open start and terminal close handling
434         * @param throwable      failure associated with the terminal outcome, or {@code null} when no failure is available
435         */
436        default void didCloseStream(@NonNull StatementContext<?> ctx,
437                                                                                                                        @NonNull StreamTerminalOutcome outcome,
438                                                                                                                        @NonNull Long rowsConsumed,
439                                                                                                                        @NonNull Duration streamDuration,
440                                                                                                                        @Nullable Throwable throwable) {
441                // No-op by default
442        }
443
444        /**
445         * Called immediately before Pyranid attempts to open a notification session.
446         * <p>
447         * Argument validation, ambient-transaction rejection, database-type resolution, and backend-specific channel
448         * validation occur before this callback. Exactly one of
449         * {@link #didOpenNotificationSession(DatabaseType, UUID, Duration)} or
450         * {@link #didFailToOpenNotificationSession(DatabaseType, UUID, Duration, Throwable)} follows it.
451         *
452         * @param databaseType         database type resolved for the notification session
453         * @param notificationSessionId identifier for this notification-session invocation
454         * @since 4.6.0
455         */
456        default void willOpenNotificationSession(@NonNull DatabaseType databaseType,
457                                                                                                                                                                        @NonNull UUID notificationSessionId) {
458                // No-op by default
459        }
460
461        /**
462         * Called after Pyranid successfully opens a notification session and registers every requested channel.
463         * <p>
464         * This callback occurs before the application operation is dispatched. It does not indicate that application
465         * reconciliation or readiness work has completed.
466         *
467         * @param databaseType          database type resolved for the notification session
468         * @param notificationSessionId identifier for this notification-session invocation
469         * @param openDuration          elapsed time from the corresponding
470         *                              {@link #willOpenNotificationSession(DatabaseType, UUID)} callback through successful
471         *                              channel registration
472         * @since 4.6.0
473         */
474        default void didOpenNotificationSession(@NonNull DatabaseType databaseType,
475                                                                                                                                                                 @NonNull UUID notificationSessionId,
476                                                                                                                                                                 @NonNull Duration openDuration) {
477                // No-op by default
478        }
479
480        /**
481         * Called after Pyranid fails to open a notification session.
482         * <p>
483         * This is the terminal lifecycle callback for an invocation that never reached
484         * {@link #didOpenNotificationSession(DatabaseType, UUID, Duration)}. It is not followed by
485         * {@link #didCloseNotificationSession(DatabaseType, UUID, NotificationSessionOutcome, Duration, Throwable)}.
486         * Pyranid emits it after selecting the opening failure and before cleaning up any acquired candidate connection.
487         *
488         * @param databaseType          database type resolved for the notification session
489         * @param notificationSessionId identifier for this notification-session invocation
490         * @param openDuration          elapsed time from the corresponding
491         *                              {@link #willOpenNotificationSession(DatabaseType, UUID)} callback through the opening
492         *                              failure
493         * @param throwable             failure that prevented the notification session from opening
494         * @since 4.6.0
495         */
496        default void didFailToOpenNotificationSession(@NonNull DatabaseType databaseType,
497                                                                                                                                                                         @NonNull UUID notificationSessionId,
498                                                                                                                                                                         @NonNull Duration openDuration,
499                                                                                                                                                                         @NonNull Throwable throwable) {
500                // No-op by default
501        }
502
503        /**
504         * Called when Pyranid delivers a nonempty notification batch to application code.
505         * <p>
506         * Empty receive results and notifications discarded during setup or cleanup are not reported.
507         *
508         * @param databaseType          database type resolved for the notification session
509         * @param notificationSessionId identifier for the notification session that delivered the batch
510         * @param notificationCount     number of notifications in the delivered batch
511         * @since 4.6.0
512         */
513        default void didDeliverNotificationBatch(@NonNull DatabaseType databaseType,
514                                                                                                                                                                        @NonNull UUID notificationSessionId,
515                                                                                                                                                                        @NonNull Long notificationCount) {
516                // No-op by default
517        }
518
519        /**
520         * Called when a receive operation detects terminal uncertainty or loss of an opened notification connection.
521         * <p>
522         * This callback is emitted at most once for a notification session. It does not report setup failures, failures
523         * from ordinary database operations performed by application code, or cleanup-only failures.
524         *
525         * @param databaseType          database type resolved for the notification session
526         * @param notificationSessionId identifier for the notification session whose connection was lost
527         * @param throwable             terminal connection failure
528         * @since 4.6.0
529         */
530        default void didLoseNotificationConnection(@NonNull DatabaseType databaseType,
531                                                                                                                                                                                @NonNull UUID notificationSessionId,
532                                                                                                                                                                                @NonNull Throwable throwable) {
533                // No-op by default
534        }
535
536        /**
537         * Called after an opened notification session reaches a terminal state and cleanup completes.
538         * <p>
539         * Every invocation that emits {@link #didOpenNotificationSession(DatabaseType, UUID, Duration)} emits this callback
540         * exactly once. Invocations that fail before opening terminate through
541         * {@link #didFailToOpenNotificationSession(DatabaseType, UUID, Duration, Throwable)} instead.
542         *
543         * @param databaseType          database type resolved for the notification session
544         * @param notificationSessionId identifier for the notification session
545         * @param outcome               terminal notification-session outcome
546         * @param sessionDuration       elapsed time from the corresponding
547         *                              {@link #willOpenNotificationSession(DatabaseType, UUID)} callback through completed
548         *                              cleanup
549         * @param throwable             selected failure for {@link NotificationSessionOutcome#FAILED}, or {@code null} for
550         *                              {@link NotificationSessionOutcome#CALLBACK_RETURNED} and
551         *                              {@link NotificationSessionOutcome#INTERRUPTED}
552         * @since 4.6.0
553         */
554        default void didCloseNotificationSession(@NonNull DatabaseType databaseType,
555                                                                                                                                                                         @NonNull UUID notificationSessionId,
556                                                                                                                                                                         @NonNull NotificationSessionOutcome outcome,
557                                                                                                                                                                         @NonNull Duration sessionDuration,
558                                                                                                                                                                         @Nullable Throwable throwable) {
559                // No-op by default
560        }
561
562        /**
563         * Returns a notification counter snapshot if this collector supports in-process inspection.
564         * <p>
565         * Implementations may read counters independently without cross-counter atomicity. Callers that need
566         * invariant-consistent snapshots should drain in-flight work before reading.
567         *
568         * @return a notification metrics snapshot, if supported
569         * @since 4.6.0
570         */
571        @NonNull
572        default Optional<NotificationSnapshot> notificationSnapshot() {
573                return Optional.empty();
574        }
575
576        /**
577         * Returns a counter snapshot if this collector supports in-process inspection.
578         * <p>
579         * Implementations may read counters independently without cross-counter atomicity. Callers that need invariant-consistent
580         * snapshots should drain in-flight work before reading.
581         *
582         * @return a metrics snapshot, if supported
583         */
584        @NonNull
585        default Optional<Snapshot> snapshot() {
586                return Optional.empty();
587        }
588
589        /**
590         * Best-effort reset for test and ad-hoc use.
591         * <p>
592         * Concurrent updates may race with reset operations. Production rolling-window semantics should be implemented outside
593         * this interface.
594         */
595        default void reset() {
596                // No-op by default
597        }
598
599        /**
600         * Returns the shared no-op metrics collector.
601         *
602         * @return no-op metrics collector used when metrics collection is disabled
603         */
604        @NonNull
605        static MetricsCollector disabledInstance() {
606                return DisabledMetricsCollector.defaultInstance();
607        }
608
609        /**
610         * Creates a fresh in-memory counter collector.
611         * <p>
612         * This collector is intended for tests and lightweight local inspection through {@link #snapshot()} and
613         * {@link #notificationSnapshot()}.
614         *
615         * @return new in-memory metrics collector
616         */
617        @NonNull
618        static MetricsCollector inMemoryInstance() {
619                return InMemoryMetricsCollector.defaultInstance();
620        }
621
622        /**
623         * Terminal outcome for an opened notification session.
624         *
625         * @since 4.6.0
626         */
627        enum NotificationSessionOutcome {
628                /**
629                 * The application callback returned and notification-session cleanup completed normally.
630                 */
631                CALLBACK_RETURNED,
632
633                /**
634                 * Cooperative interruption ended the notification session and cleanup completed normally.
635                 */
636                INTERRUPTED,
637
638                /**
639                 * Setup after opening, notification receive, application callback, or cleanup failed.
640                 */
641                FAILED
642        }
643
644        /**
645         * Counter snapshot for notification metrics.
646         * <p>
647         * Implementations may read counters independently without cross-counter atomicity. Callers that need
648         * invariant-consistent snapshots should drain in-flight work before reading.
649         * <p>
650         * Instances are immutable value objects created through {@link #of(Long, Long, Long, Long, Long, Long, Long)}.
651         * Future releases may add counters and accessors while retaining this factory with defined defaults for any
652         * newly added values.
653         *
654         * @since 4.6.0
655         */
656        @ThreadSafe
657        final class NotificationSnapshot {
658                @NonNull
659                private final Long sessionsStarted;
660                @NonNull
661                private final Long sessionsOpened;
662                @NonNull
663                private final Long sessionsCallbackReturned;
664                @NonNull
665                private final Long sessionsInterrupted;
666                @NonNull
667                private final Long sessionsFailed;
668                @NonNull
669                private final Long batchesDelivered;
670                @NonNull
671                private final Long notificationsDelivered;
672
673                private NotificationSnapshot(@NonNull Long sessionsStarted,
674                                                                                                                 @NonNull Long sessionsOpened,
675                                                                                                                 @NonNull Long sessionsCallbackReturned,
676                                                                                                                 @NonNull Long sessionsInterrupted,
677                                                                                                                 @NonNull Long sessionsFailed,
678                                                                                                                 @NonNull Long batchesDelivered,
679                                                                                                                 @NonNull Long notificationsDelivered) {
680                        this.sessionsStarted = requireNonNull(sessionsStarted);
681                        this.sessionsOpened = requireNonNull(sessionsOpened);
682                        this.sessionsCallbackReturned = requireNonNull(sessionsCallbackReturned);
683                        this.sessionsInterrupted = requireNonNull(sessionsInterrupted);
684                        this.sessionsFailed = requireNonNull(sessionsFailed);
685                        this.batchesDelivered = requireNonNull(batchesDelivered);
686                        this.notificationsDelivered = requireNonNull(notificationsDelivered);
687                }
688
689                /**
690                 * Creates a notification metrics snapshot.
691                 *
692                 * @param sessionsStarted          valid, type-resolved notification-session invocations that entered measured setup
693                 * @param sessionsOpened           notification sessions that completed channel registration
694                 * @param sessionsCallbackReturned opened notification sessions whose callbacks returned and cleanup completed normally
695                 * @param sessionsInterrupted      opened notification sessions that ended through clean cooperative interruption
696                 * @param sessionsFailed           notification-session invocations that failed before opening or after opening
697                 * @param batchesDelivered         nonempty notification batches delivered to application code
698                 * @param notificationsDelivered   notifications contained in delivered batches
699                 * @return notification metrics snapshot
700                 * @throws NullPointerException if any counter is null
701                 * @since 4.6.0
702                 */
703                @NonNull
704                public static NotificationSnapshot of(@NonNull Long sessionsStarted,
705                                                                                                                                                                                         @NonNull Long sessionsOpened,
706                                                                                                                                                                                         @NonNull Long sessionsCallbackReturned,
707                                                                                                                                                                                         @NonNull Long sessionsInterrupted,
708                                                                                                                                                                                         @NonNull Long sessionsFailed,
709                                                                                                                                                                                         @NonNull Long batchesDelivered,
710                                                                                                                                                                                         @NonNull Long notificationsDelivered) {
711                        return new NotificationSnapshot(sessionsStarted, sessionsOpened, sessionsCallbackReturned, sessionsInterrupted,
712                                        sessionsFailed, batchesDelivered, notificationsDelivered);
713                }
714
715                /**
716                 * @return valid, type-resolved notification-session invocations that entered measured setup
717                 * @since 4.6.0
718                 */
719                @NonNull
720                public Long sessionsStarted() {
721                        return this.sessionsStarted;
722                }
723
724                /**
725                 * @return notification sessions that completed channel registration
726                 * @since 4.6.0
727                 */
728                @NonNull
729                public Long sessionsOpened() {
730                        return this.sessionsOpened;
731                }
732
733                /**
734                 * @return opened notification sessions whose callbacks returned and cleanup completed normally
735                 * @since 4.6.0
736                 */
737                @NonNull
738                public Long sessionsCallbackReturned() {
739                        return this.sessionsCallbackReturned;
740                }
741
742                /**
743                 * @return opened notification sessions that ended through clean cooperative interruption
744                 * @since 4.6.0
745                 */
746                @NonNull
747                public Long sessionsInterrupted() {
748                        return this.sessionsInterrupted;
749                }
750
751                /**
752                 * @return notification-session invocations that failed before opening or after opening
753                 * @since 4.6.0
754                 */
755                @NonNull
756                public Long sessionsFailed() {
757                        return this.sessionsFailed;
758                }
759
760                /**
761                 * @return nonempty notification batches delivered to application code
762                 * @since 4.6.0
763                 */
764                @NonNull
765                public Long batchesDelivered() {
766                        return this.batchesDelivered;
767                }
768
769                /**
770                 * @return notifications contained in delivered batches
771                 * @since 4.6.0
772                 */
773                @NonNull
774                public Long notificationsDelivered() {
775                        return this.notificationsDelivered;
776                }
777
778                @Override
779                public boolean equals(@Nullable Object object) {
780                        if (this == object)
781                                return true;
782
783                        if (!(object instanceof NotificationSnapshot notificationSnapshot))
784                                return false;
785
786                        return sessionsStarted().equals(notificationSnapshot.sessionsStarted())
787                                        && sessionsOpened().equals(notificationSnapshot.sessionsOpened())
788                                        && sessionsCallbackReturned().equals(notificationSnapshot.sessionsCallbackReturned())
789                                        && sessionsInterrupted().equals(notificationSnapshot.sessionsInterrupted())
790                                        && sessionsFailed().equals(notificationSnapshot.sessionsFailed())
791                                        && batchesDelivered().equals(notificationSnapshot.batchesDelivered())
792                                        && notificationsDelivered().equals(notificationSnapshot.notificationsDelivered());
793                }
794
795                @Override
796                public int hashCode() {
797                        return Objects.hash(sessionsStarted(), sessionsOpened(), sessionsCallbackReturned(), sessionsInterrupted(),
798                                        sessionsFailed(), batchesDelivered(), notificationsDelivered());
799                }
800
801                @Override
802                @NonNull
803                public String toString() {
804                        return "NotificationSnapshot[" +
805                                        "sessionsStarted=" + sessionsStarted() +
806                                        ", sessionsOpened=" + sessionsOpened() +
807                                        ", sessionsCallbackReturned=" + sessionsCallbackReturned() +
808                                        ", sessionsInterrupted=" + sessionsInterrupted() +
809                                        ", sessionsFailed=" + sessionsFailed() +
810                                        ", batchesDelivered=" + batchesDelivered() +
811                                        ", notificationsDelivered=" + notificationsDelivered() +
812                                        ']';
813                }
814        }
815
816        /**
817         * Logical outcome for a closure-based transaction.
818         */
819        enum TransactionClosureOutcome {
820                /**
821                 * The transaction used a physical JDBC transaction and commit completed normally.
822                 */
823                COMMITTED,
824
825                /**
826                 * The transaction used a physical JDBC transaction and was rolled back, including a recognized commit-time
827                 * serialization failure whose follow-up rollback completed normally.
828                 */
829                ROLLED_BACK,
830
831                /**
832                 * The transaction closure exited without ever acquiring a JDBC connection.
833                 */
834                NO_PHYSICAL_TX,
835
836                /**
837                 * The transaction failed before Pyranid could report a normal commit or rollback outcome.
838                 */
839                FAILED
840        }
841
842        /**
843         * Physical transaction begin phase that failed.
844         */
845        enum PhysicalTransactionBeginFailurePhase {
846                /**
847                 * Connection acquisition from the configured {@link javax.sql.DataSource} failed.
848                 */
849                ACQUIRE_CONNECTION,
850
851                /**
852                 * Reading the connection's initial autocommit setting failed.
853                 */
854                READ_INITIAL_AUTOCOMMIT,
855
856                /**
857                 * Reading the connection's initial transaction isolation failed.
858                 */
859                READ_INITIAL_ISOLATION,
860
861                /**
862                 * Reading the connection's initial read-only setting failed.
863                 */
864                READ_INITIAL_READ_ONLY,
865
866                /**
867                 * Disabling autocommit for the transaction failed.
868                 */
869                SET_AUTOCOMMIT_FALSE,
870
871                /**
872                 * Applying the requested transaction isolation failed.
873                 */
874                SET_ISOLATION,
875
876                /**
877                 * Applying the requested read-only setting failed.
878                 */
879                SET_READ_ONLY
880        }
881
882        /**
883         * Terminal outcome for an opened stream.
884         */
885        enum StreamTerminalOutcome {
886                /**
887                 * Stream iteration reached the end of the result set and cleanup completed without an iteration or callback failure.
888                 */
889                COMPLETED_NORMALLY,
890
891                /**
892                 * The stream was closed before all rows were consumed.
893                 */
894                EARLY_CLOSE,
895
896                /**
897                 * The caller-provided stream callback failed.
898                 */
899                CALLBACK_FAILURE,
900
901                /**
902                 * Result-set iteration failed while the stream was open.
903                 */
904                ITERATION_FAILURE,
905
906                /**
907                 * The stream failed before it opened.
908                 * <p>
909                 * Pyranid normally reports this through {@link #didFailToOpenStream(StatementContext, DatabaseType, Duration,
910                 * Throwable)} instead of {@link #didCloseStream(StatementContext, StreamTerminalOutcome, Long, Duration, Throwable)}.
911                 */
912                OPEN_FAILURE
913        }
914
915        /**
916         * Counter snapshot for collectors that support in-process inspection.
917         * <p>
918         * Implementations may read counters independently without cross-counter atomicity. Callers that need invariant-consistent
919         * snapshots should drain in-flight work before reading.
920         *
921         * @param connectionsAcquiredStatementScope          statement-scoped connection acquisitions that completed normally
922         * @param connectionsAcquiredTransactionScope        transaction-scoped connection acquisitions that completed normally
923         * @param connectionsFailedStatementScope            statement-scoped connection acquisitions that failed
924         * @param connectionsFailedTransactionScope          transaction-scoped connection acquisitions that failed
925         * @param connectionReleaseFailuresStatementScope    statement-scoped connection releases that failed
926         * @param connectionReleaseFailuresTransactionScope  transaction-scoped connection releases that failed
927         * @param transactionClosuresEntered                 closure-based transactions entered
928         * @param transactionClosuresExited                  closure-based transactions exited
929         * @param transactionClosuresCommitted               closure-based transactions that committed
930         * @param transactionClosuresRolledBack              closure-based transactions that rolled back
931         * @param transactionClosuresNoPhysical              closure-based transactions that exited without a physical JDBC transaction
932         * @param transactionClosuresFailed                  closure-based transactions that failed before a normal commit or rollback outcome
933         * @param physicalTransactionsBegun                  physical JDBC transactions begun
934         * @param physicalTransactionsBeginFailed            physical JDBC transactions that failed while beginning
935         * @param physicalTransactionsCommitted              physical JDBC transactions committed
936         * @param physicalTransactionsCommitFailed           physical JDBC transactions whose commit failed
937         * @param physicalTransactionsRolledBack             physical JDBC transactions rolled back
938         * @param physicalTransactionsRollbackFailed         physical JDBC transactions whose rollback failed
939         * @param savepointsCreated                          transaction savepoints created
940         * @param savepointsRolledBack                       transaction savepoints rolled back to
941         * @param savepointsReleased                         transaction savepoints released
942         * @param statementsExecuted                         statements that executed successfully
943         * @param statementsFailed                           statements that failed during execution
944         * @param streamsOpened                              streams that opened successfully
945         * @param streamsOpenFailures                        streams that failed before opening
946         * @param streamsClosedNormally                      opened streams that reached the end of the result set
947         * @param streamsEarlyClosed                         opened streams that closed before all rows were consumed
948         * @param streamsCallbackFailed                      opened streams whose caller-provided callback failed
949         * @param streamsIterationFailed                     opened streams whose result-set iteration failed
950         * @param postTransactionOperationsRun               post-transaction operations that ran
951         * @param postTransactionOperationsFailed            post-transaction operations that failed
952         */
953        @ThreadSafe
954        record Snapshot(@NonNull Long connectionsAcquiredStatementScope,
955                                                                        @NonNull Long connectionsAcquiredTransactionScope,
956                                                                        @NonNull Long connectionsFailedStatementScope,
957                                                                        @NonNull Long connectionsFailedTransactionScope,
958                                                                        @NonNull Long connectionReleaseFailuresStatementScope,
959                                                                        @NonNull Long connectionReleaseFailuresTransactionScope,
960                                                                        @NonNull Long transactionClosuresEntered,
961                                                                        @NonNull Long transactionClosuresExited,
962                                                                        @NonNull Long transactionClosuresCommitted,
963                                                                        @NonNull Long transactionClosuresRolledBack,
964                                                                        @NonNull Long transactionClosuresNoPhysical,
965                                                                        @NonNull Long transactionClosuresFailed,
966                                                                        @NonNull Long physicalTransactionsBegun,
967                                                                        @NonNull Long physicalTransactionsBeginFailed,
968                                                                        @NonNull Long physicalTransactionsCommitted,
969                                                                        @NonNull Long physicalTransactionsCommitFailed,
970                                                                        @NonNull Long physicalTransactionsRolledBack,
971                                                                        @NonNull Long physicalTransactionsRollbackFailed,
972                                                                        @NonNull Long savepointsCreated,
973                                                                        @NonNull Long savepointsRolledBack,
974                                                                        @NonNull Long savepointsReleased,
975                                                                        @NonNull Long statementsExecuted,
976                                                                        @NonNull Long statementsFailed,
977                                                                        @NonNull Long streamsOpened,
978                                                                        @NonNull Long streamsOpenFailures,
979                                                                        @NonNull Long streamsClosedNormally,
980                                                                        @NonNull Long streamsEarlyClosed,
981                                                                        @NonNull Long streamsCallbackFailed,
982                                                                        @NonNull Long streamsIterationFailed,
983                                                                        @NonNull Long postTransactionOperationsRun,
984                                                                        @NonNull Long postTransactionOperationsFailed) {
985        }
986}