Skip to content

Isolate logging state and normalize enum identifiers#438

Merged
binaryfire merged 24 commits into
0.4from
audit/logging-and-enum-identifiers
Jul 16, 2026
Merged

Isolate logging state and normalize enum identifiers#438
binaryfire merged 24 commits into
0.4from
audit/logging-and-enum-identifiers

Conversation

@binaryfire

Copy link
Copy Markdown
Collaborator

Summary

This change corrects two framework-wide ownership boundaries that become observable in long-lived Swoole workers.

First, it keeps logger graphs cached at worker scope while moving their mutable request state to coroutine ownership. Logger context, recursion depth, fingers-crossed buffers, request UIDs, and in-flight exception reporting can no longer bleed between concurrent requests. Built-in stream handlers also stop using process-global PHP error handlers around hooked filesystem operations that may yield.

Second, it makes enum-capable identifier APIs truthful end to end. Enum values are converted only where a package owns a string identifier boundary, and an explicit identifier of 0 is no longer mistaken for an absent value later in a manager, cache, queue, database, command, authorization, or realtime path.

The result preserves Laravel-facing call shapes and existing worker-level caches while removing request-state races, delayed type failures, wrong-default selection, and partial identifier normalization.

For more details, see docs/plans/2026-07-12-framework-coroutine-state-lifecycle-audit-ledger.md.

Logging ownership

Hypervel intentionally retains configured Monolog graphs across requests. Rebuilding or cloning those graphs per request would discard one of the useful properties of the worker model. The mutable parts of those graphs, however, cannot share the same lifetime.

This change:

  • gives each logger family a stable worker identity and coroutine-local visible context and recursion depth;
  • preserves context when coroutine state is copied while resetting transient recursion bookkeeping;
  • provides compatible fingers-crossed and UID components whose buffers, buffering state, and request identifiers follow coroutine ownership;
  • replaces process-global warning interception in built-in stream and rotating-file handlers with checked native operation boundaries;
  • tracks the currently reported exception in coroutine context and integrates structured exception context with JSON logs;
  • constructs on-demand channels without retaining a synthetic named logger and applies taps from the resolved configuration;
  • hydrates missing queue context as an explicit clear when a repository already exists; and
  • serializes background and deferred queue payloads at the scheduling boundary so later execution observes the dispatched job and context snapshot.

Custom handlers, processors, and complete custom drivers remain application-owned. Exact built-in Monolog classes are normalized; subclasses are not silently rewritten.

Manager and identifier contracts

Framework managers now share one callback-rebinding contract. Ordinary closures retain manager binding, while static closures and first-class callables remain valid instead of failing at registration time.

Enum identifiers are normalized at the receiver that requires a string rather than by changing the general enum_value() helper. This distinction matters because enum values used as model data, query bindings, serialized values, groups, and other value-domain inputs must retain their native integer or string representation.

The corrected identifier flow covers:

  • manager contracts, implementations, facades, and fakes;
  • cache keys, tags, events, failover reads, Redis tagged stores, and multi-key reconstruction;
  • bus, event, broadcast, mailable, batch, and chain carriers;
  • queue routing, commands, persistent backends, Horizon, and Telescope observation;
  • database managers, pooled connections, models, factories, migrations, schema commands, testing helpers, and pipeline transactions;
  • filesystem disks, scheduling, cookies, authorization, permissions, and generators; and
  • Inertia, Reverb presence state, routing, Sanctum, Scout, Testbench, translation, and monitoring boundaries.

Each changed defaulting expression preserves the owning Laravel behavior: boundaries that treat null and the empty string as default continue to do so, while null-only boundaries remain null-only. The correction is specifically that the string identifier 0 survives delegation.

Compatibility

No Laravel-facing method is removed or renamed, and no configuration key changes. Existing string and null call sites retain their behavior. Enum-capable APIs either catch up to the current Laravel contract or begin honoring unions that Hypervel already advertised.

The Reverb and translation corrections follow the same exact-absence rule without introducing a generic identifier abstraction. Cookie normalization still delegates validity to Symfony, so an enum-backed name has the same acceptance or rejection as its direct string equivalent.

Performance

The design keeps worker-cached managers, logger graphs, cache repositories, and pooled resources intact.

Ordinary string manager lookups add at most one predictable enum check. Existing enum-aware paths complete conversion they already attempted. Logging state lookups occur only when a record is handled, and feature-specific lookups apply only to channels configured with the corresponding buffering or UID component. There are no request-wide locks, registries, logger rebuilds, pools, or coordination layers.

Validation

Regression coverage exercises concurrent logger state, recursive logging, copied coroutine context, buffered handlers, UID replication, native stream failures, dynamic channel construction, queued payload timing, callback forms, enum representations, explicit zero identifiers, and direct string interoperability across the affected packages.

The formatter and static-analysis gates are clean, and the implementation has been exercised through the complete framework test workflow as well as focused package suites.

Document the framework-wide rule for converting enum-backed identifiers only at their owning string boundaries. Record compatibility, performance, implementation, regression, and completeness requirements for manager, cache, queue, database, filesystem, authorization, and related package surfaces.
Use one callback-rebinding contract across framework managers so static closures, first-class callables, and ordinary closures retain Laravel-compatible extension behavior. Normalize enum-backed driver names at manager-owned string boundaries, preserve explicit zero identifiers, align contracts, facades, and fakes, and cover inherited and package-specific manager behavior.
Replace worker-shared logger context and Monolog recursion bookkeeping with one coroutine-owned logger-family state. Preserve visible context across replicated execution while resetting transient recursion depth, prevent object-ID reuse from aliasing destroyed loggers, and retain Monolog loop-detection behavior without cross-request interference.
Provide vendor-compatible fingers-crossed and UID components whose mutable request state follows coroutine ownership instead of worker lifetime. Replicated contexts begin with safe buffer state while retaining request identity, and exact-class normalization leaves user-defined Monolog subclasses under application ownership.
Replace process-global PHP error-handler windows around yielding stream operations with checked native boundaries. Preserve Monolog retry, locking, permission, rotation, retention, and diagnostic behavior while preventing one coroutine from intercepting warnings raised by another.
Port structured exception context into the Log formatter while keeping in-flight reporting state coroutine-local. Preserve nested reporting ownership across copied contexts, restore prior state in finally blocks, and avoid recursive context expansion when the same exception is already being reported.
Build dynamic channels without retaining a synthetic named logger, apply taps from the resolved configuration, and confine manager-wide context mutation to cached named channels. Normalize only exact built-in mutable Monolog components, restore named-stack behavior, preserve zero-named channels, and document worker-cached custom component ownership.
Serialize background and deferred jobs when execution is scheduled so mutable job and context state cannot drift before a later coroutine or timer runs. Reuse the synchronous payload executor, preserve after-commit timing, surface serialization failures at the owning boundary, and clear stale log context when an incoming payload carries none.
Remove the package-local action-level wrapping workaround and let the Log package own safe handler normalization. Preserve Sentry channel behavior while ensuring buffering state follows the same coroutine-local contract as every other framework-built Monolog graph.
Convert enum-backed keys and tags only where repositories, stores, events, and Redis commands require strings. Preserve numeric-string multi-read keys, align event metadata with concrete cache APIs, keep failover raw reads truthful, retain direct string interoperability, and avoid rebuilding value-domain arrays or adding work around cache I/O.
Normalize enum-backed connection and queue names before they enter typed carrier state, and preserve explicit zero identifiers through batches, chains, queued closures, broadcasts, and mailables. Keep value-oriented enum groups intact, retain established empty-string behavior, and cover both immediate configuration and later serialized execution.
Carry normalized connection and queue names through manager lookup, routing, payload creation, storage, reservation, returned jobs, commands, and Telescope observation. Replace truthiness with each boundary established null or empty sentinel so queue zero remains addressable without changing normal default selection.
Keep explicit zero-valued connection and queue names through Horizon command parsing, supervisor options, queue clearing, and wait-time lookup. Match Queue behavior at each boundary while retaining the existing fallback for genuinely absent or empty command input.
Convert enum-backed connection and table identifiers at database-owned string boundaries and preserve zero-valued names through managers, pools, models, factories, migrations, schema commands, testing helpers, and pipeline transactions. Keep query values and bindings untouched while correcting index and command defaults that previously discarded valid zero identifiers.
Normalize queue, connection, cache-store, and timezone identifiers when schedules are configured so typed state remains valid and zero-backed enums are not replaced by defaults. Preserve DateTimeZone objects and existing scheduling behavior for ordinary strings and absent values.
Normalize enum abilities before authorization inference and preserve guard name zero through discovery, middleware parsing, role scopes, mismatch checks, Passport compatibility, and permission diagnostics. Keep application-level guard semantics and existing empty-name fallback behavior unchanged.
Convert enum cookie names to their string representation for request lookup, creation, queued lookup, and removal. Preserve downstream Symfony validation exactly, including equivalent rejection of invalid direct-string and enum-backed names, rather than bypassing domain-owned constraints.
Treat presence user ID zero as a real identifier during subscription, unsubscription, client rebroadcast, and webhook payload construction. Normalize stored IDs at the owning boundary and reserve null, rather than truthiness, for complete channel data lookup and missing shared state.
Normalize once keys, token cache stores, search connections, and Telescope tags where their owning services require strings. Preserve ordinary values unchanged and ensure integer-backed enum identifiers remain stable through persistence, monitoring, and command execution.
Use exact absence checks when converting routes and resolving translation namespaces so explicit string zero remains addressable. Preserve the established null and empty-string fallback behavior while preventing valid names from being silently replaced.
Preserve explicit zero-valued model and database names through policy generation and Testbench SQLite commands. Keep existing defaults for absent input while ensuring command arguments select the requested target instead of silently falling back.
Mark the Log package audit complete and record the final logging-state, callback, queue-capture, enum-identifier, zero-sentinel, compatibility, performance, and regression decisions. Keep the reusable audit checklist and durable findings ledger aligned with the implemented framework boundaries.
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 231 files, which is 181 over the limit of 50.

To get a review, narrow the scope:
• coderabbit review --type committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

Upgrade to a paid plan to raise the limit.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2fedba7d-3c09-49ee-81f4-1c6776ff12a0

📥 Commits

Reviewing files that changed from the base of the PR and between 1f1f833 and 4be7f9a.

📒 Files selected for processing (231)
  • docs/plans/2026-07-12-framework-coroutine-state-lifecycle-audit-ledger.md
  • docs/plans/2026-07-12-framework-coroutine-state-lifecycle-audit.md
  • docs/plans/2026-07-15-framework-enum-identifier-contracts.md
  • src/auth/src/AuthManager.php
  • src/boost/docs/logging.md
  • src/broadcasting/src/AnonymousEvent.php
  • src/broadcasting/src/BroadcastManager.php
  • src/broadcasting/src/InteractsWithBroadcasting.php
  • src/broadcasting/src/PendingBroadcast.php
  • src/bus/src/ChainedBatch.php
  • src/bus/src/PendingBatch.php
  • src/bus/src/Queueable.php
  • src/cache/src/CacheManager.php
  • src/cache/src/Events/CacheEvent.php
  • src/cache/src/Events/CacheHit.php
  • src/cache/src/Events/KeyWriteFailed.php
  • src/cache/src/Events/KeyWritten.php
  • src/cache/src/Events/WritingKey.php
  • src/cache/src/FailoverStore.php
  • src/cache/src/Redis/AllTaggedCache.php
  • src/cache/src/Redis/AnyTaggedCache.php
  • src/cache/src/Redis/Console/BenchmarkCommand.php
  • src/cache/src/Redis/Console/Concerns/DetectsRedisStore.php
  • src/cache/src/Redis/Console/DoctorCommand.php
  • src/cache/src/Repository.php
  • src/cache/src/RetrievesMultipleKeys.php
  • src/cache/src/StackTaggedCache.php
  • src/cache/src/TaggedCache.php
  • src/concurrency/src/ConcurrencyManager.php
  • src/console/src/Scheduling/ManagesFrequencies.php
  • src/console/src/Scheduling/Schedule.php
  • src/contracts/src/Broadcasting/Factory.php
  • src/contracts/src/Debug/ExceptionHandler.php
  • src/contracts/src/Mail/Factory.php
  • src/contracts/src/Notifications/Factory.php
  • src/contracts/src/Queue/Factory.php
  • src/cookie/src/CookieJar.php
  • src/database/src/Connection.php
  • src/database/src/ConnectionResolver.php
  • src/database/src/Console/MonitorCommand.php
  • src/database/src/Console/Seeds/SeedCommand.php
  • src/database/src/DatabaseManager.php
  • src/database/src/Eloquent/Factories/Factory.php
  • src/database/src/Eloquent/Model.php
  • src/database/src/Migrations/Migrator.php
  • src/database/src/Schema/Blueprint.php
  • src/database/src/SimpleConnectionResolver.php
  • src/events/src/Dispatcher.php
  • src/events/src/QueuedClosure.php
  • src/filesystem/src/FilesystemManager.php
  • src/foundation/src/Auth/Access/AuthorizesRequests.php
  • src/foundation/src/Bus/PendingChain.php
  • src/foundation/src/Console/PolicyMakeCommand.php
  • src/foundation/src/Exceptions/Handler.php
  • src/foundation/src/MaintenanceModeManager.php
  • src/foundation/src/Testing/Concerns/InteractsWithDatabase.php
  • src/foundation/src/Testing/DatabaseConnectionResolver.php
  • src/foundation/src/helpers.php
  • src/horizon/src/Console/ClearCommand.php
  • src/horizon/src/Console/SupervisorCommand.php
  • src/horizon/src/SupervisorOptions.php
  • src/horizon/src/WaitTimeCalculator.php
  • src/inertia/src/ResolvesOnce.php
  • src/log/README.md
  • src/log/composer.json
  • src/log/src/Context/ContextServiceProvider.php
  • src/log/src/Context/Repository.php
  • src/log/src/Formatters/JsonFormatter.php
  • src/log/src/Handlers/Concerns/PerformsSafeStreamOperations.php
  • src/log/src/Handlers/FingersCrossedHandler.php
  • src/log/src/Handlers/FingersCrossedState.php
  • src/log/src/Handlers/RotatingFileHandler.php
  • src/log/src/Handlers/StreamHandler.php
  • src/log/src/LogManager.php
  • src/log/src/Logger.php
  • src/log/src/LoggerState.php
  • src/log/src/Processors/UidProcessor.php
  • src/mail/src/MailManager.php
  • src/mail/src/Mailable.php
  • src/notifications/src/ChannelManager.php
  • src/permission/src/Commands/ShowCommand.php
  • src/permission/src/Guard.php
  • src/permission/src/Middleware/PermissionMiddleware.php
  • src/permission/src/Models/Role.php
  • src/permission/src/Traits/HasRoles.php
  • src/pipeline/src/Hub.php
  • src/queue/src/BackgroundQueue.php
  • src/queue/src/BeanstalkdQueue.php
  • src/queue/src/Console/ClearCommand.php
  • src/queue/src/Console/Concerns/ParsesQueue.php
  • src/queue/src/Console/ListenCommand.php
  • src/queue/src/Console/WorkCommand.php
  • src/queue/src/DatabaseQueue.php
  • src/queue/src/DeferredQueue.php
  • src/queue/src/QueueManager.php
  • src/queue/src/QueueRoutes.php
  • src/queue/src/RedisQueue.php
  • src/queue/src/SqsQueue.php
  • src/queue/src/SyncQueue.php
  • src/redis/src/RedisManager.php
  • src/reverb/src/Protocols/Pusher/Channels/ChannelConnection.php
  • src/reverb/src/Protocols/Pusher/Channels/Concerns/InteractsWithPresenceChannels.php
  • src/reverb/src/Protocols/Pusher/ClientEvent.php
  • src/routing/src/AbstractRouteCollection.php
  • src/sanctum/src/PersonalAccessToken.php
  • src/scout/src/Console/SyncIndexSettingsCommand.php
  • src/sentry/src/LogChannel.php
  • src/sentry/src/Logs/LogChannel.php
  • src/session/src/SessionManager.php
  • src/socialite/src/Contracts/Factory.php
  • src/socialite/src/Socialite.php
  • src/socialite/src/SocialiteManager.php
  • src/socialite/src/Testing/SocialiteFake.php
  • src/support/src/Facades/Broadcast.php
  • src/support/src/Facades/Cache.php
  • src/support/src/Facades/Concurrency.php
  • src/support/src/Facades/Hash.php
  • src/support/src/Facades/Jwt.php
  • src/support/src/Facades/Mail.php
  • src/support/src/Facades/MaintenanceMode.php
  • src/support/src/Facades/Notification.php
  • src/support/src/Facades/Queue.php
  • src/support/src/Facades/Session.php
  • src/support/src/Facades/Storage.php
  • src/support/src/Manager.php
  • src/support/src/MultipleInstanceManager.php
  • src/support/src/RebindsCallbacksToSelf.php
  • src/support/src/Testing/Fakes/MailFake.php
  • src/support/src/Testing/Fakes/NotificationFake.php
  • src/telescope/src/Watchers/ClientRequestWatcher.php
  • src/telescope/src/Watchers/JobWatcher.php
  • src/testbench/src/Foundation/Console/CreateSqliteDbCommand.php
  • src/testbench/src/Foundation/Console/DropSqliteDbCommand.php
  • src/translation/src/ArrayLoader.php
  • tests/Broadcasting/InteractsWithBroadcastingTest.php
  • tests/Broadcasting/PendingBroadcastTest.php
  • tests/Bus/BusPendingBatchTest.php
  • tests/Bus/QueueableTest.php
  • tests/Cache/CacheManagerTest.php
  • tests/Cache/CacheRepositoryEnumTest.php
  • tests/Cache/CacheStackStoreTagsTest.php
  • tests/Cache/CacheTaggedCacheTest.php
  • tests/Cache/ConcurrencyLimiterTest.php
  • tests/Cache/Redis/AllTaggedCacheTest.php
  • tests/Cache/Redis/AnyTaggedCacheTest.php
  • tests/Cache/Redis/Console/BenchmarkCommandTest.php
  • tests/Cache/Redis/Console/DoctorCommandTest.php
  • tests/Console/Scheduling/EventTest.php
  • tests/Console/Scheduling/ScheduleTest.php
  • tests/Container/ContextualAttributeBindingTest.php
  • tests/Cookie/CookieJarTest.php
  • tests/Database/DatabaseConnectionTest.php
  • tests/Database/DatabaseEloquentFactoryTest.php
  • tests/Database/DatabaseEloquentModelAttributesTest.php
  • tests/Database/DatabaseEloquentModelTest.php
  • tests/Database/DatabaseManagerTest.php
  • tests/Database/DatabaseMigratorConnectionRoutingTest.php
  • tests/Database/DatabaseMonitorCommandTest.php
  • tests/Database/DatabaseSchemaBlueprintTest.php
  • tests/Database/SeedCommandTest.php
  • tests/Events/QueuedEventsTest.php
  • tests/Foundation/FoundationAuthorizesRequestsTraitTest.php
  • tests/Foundation/FoundationExceptionsHandlerTest.php
  • tests/Foundation/FoundationHelpersTest.php
  • tests/Foundation/PendingChainTest.php
  • tests/Foundation/Testing/Concerns/InteractsWithDatabaseTest.php
  • tests/Foundation/Testing/DatabaseConnectionResolverTest.php
  • tests/Inertia/OncePropTest.php
  • tests/Integration/Broadcasting/BroadcastManagerTest.php
  • tests/Integration/Broadcasting/SendingBroadcastsViaAnonymousEventTest.php
  • tests/Integration/Cache/FailoverStoreTest.php
  • tests/Integration/Concurrency/ConcurrencyTest.php
  • tests/Integration/Console/JobSchedulingTest.php
  • tests/Integration/Events/QueuedClosureListenerTest.php
  • tests/Integration/Filesystem/StorageFakeTest.php
  • tests/Integration/Generators/PolicyMakeCommandTest.php
  • tests/Integration/Horizon/Feature/ClearCommandTest.php
  • tests/Integration/Horizon/Feature/SupervisorCommandTest.php
  • tests/Integration/Horizon/Feature/SupervisorOptionsTest.php
  • tests/Integration/Horizon/Feature/WaitTimeCalculatorTest.php
  • tests/Integration/Queue/WorkCommandTest.php
  • tests/Integration/Reverb/ServerTest.php
  • tests/Jwt/JwtManagerTest.php
  • tests/Log/ContextQueueTest.php
  • tests/Log/FingersCrossedHandlerTest.php
  • tests/Log/JsonFormatterTest.php
  • tests/Log/LogLoggerTest.php
  • tests/Log/LogManagerTest.php
  • tests/Log/StreamHandlerTest.php
  • tests/Log/UidProcessorTest.php
  • tests/Mail/MailManagerTest.php
  • tests/Mail/MailableQueuedTest.php
  • tests/Notifications/NotificationChannelManagerTest.php
  • tests/Permission/Commands/CommandTest.php
  • tests/Permission/GuardTest.php
  • tests/Permission/Middleware/PermissionMiddlewareTest.php
  • tests/Permission/Models/RoleTest.php
  • tests/Permission/Traits/HasRolesTest.php
  • tests/Pipeline/PipelineTest.php
  • tests/Queue/FailoverQueueTest.php
  • tests/Queue/QueueBackgroundQueueTest.php
  • tests/Queue/QueueBeanstalkdQueueTest.php
  • tests/Queue/QueueCommandIdentifierTest.php
  • tests/Queue/QueueDatabaseQueueUnitTest.php
  • tests/Queue/QueueDeferredQueueTest.php
  • tests/Queue/QueueManagerTest.php
  • tests/Queue/QueuePauseResumeTest.php
  • tests/Queue/QueueRedisQueueTest.php
  • tests/Queue/QueueRoutesTest.php
  • tests/Queue/QueueSqsQueueTest.php
  • tests/Queue/QueueWorkerTest.php
  • tests/Redis/RedisManagerTest.php
  • tests/Reverb/Protocols/Pusher/Channels/ChannelConnectionTest.php
  • tests/Reverb/Protocols/Pusher/Channels/PresenceCacheChannelTest.php
  • tests/Reverb/Protocols/Pusher/Channels/PresenceChannelTest.php
  • tests/Reverb/Protocols/Pusher/ClientEventTest.php
  • tests/Routing/RouteCollectionTest.php
  • tests/Sanctum/PersonalAccessTokenCacheTest.php
  • tests/Scout/Unit/Console/SyncIndexSettingsCommandTest.php
  • tests/Sentry/LogChannelTest.php
  • tests/Session/SessionManagerTest.php
  • tests/Socialite/SocialiteFakeTest.php
  • tests/Socialite/SocialiteManagerTest.php
  • tests/Support/ManagerTest.php
  • tests/Support/RebindsCallbacksToSelfTest.php
  • tests/Support/SupportMaintenanceModeTest.php
  • tests/Telescope/Watchers/ClientRequestWatcherTest.php
  • tests/Telescope/Watchers/JobWatcherTest.php
  • tests/Testbench/Foundation/Console/CreateSqliteDbCommandTest.php
  • tests/Testbench/Foundation/Console/DropSqliteDbCommandTest.php
  • tests/Translation/TranslationArrayLoaderTest.php

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Review on demand using usage pricing
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch audit/logging-and-enum-identifiers

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 16, 2026

Copy link
Copy Markdown

Greptile Summary

This large PR addresses two framework-wide concerns for long-lived Swoole workers: it isolates mutable logging state (context, recursion depth, fingers-crossed buffers, UID processors) into coroutine-local ownership while keeping the immutable logger graphs cached at worker scope, and it normalizes enum identifiers to strings at each receiver boundary that requires a string key rather than converting them globally.

  • Logging isolation: New LoggerState, FingersCrossedState, FingersCrossedHandler, StreamHandler, RotatingFileHandler, and UidProcessor types store per-request mutable state in CoroutineContext, replacing process-global error handlers around stream operations with try/catch, and adding a coroutine-local recursion guard that supersedes Monolog's instance-global loop detector.
  • Enum identifier normalization: Replaces enum_value($key) (which can return an integer for int-backed enums) with $key instanceof UnitEnum ? (string) enum_value($key) : $key at every manager, cache, queue, and other identifier boundary that requires a string, preserving integer enum values where they are used as data rather than identifiers.
  • Queue payload serialization: BackgroundQueue and DeferredQueue now serialize job payloads at dispatch time via createPayload(), so the executing coroutine observes the dispatched job and context snapshot rather than the live request state.

Confidence Score: 5/5

Safe to merge. The changes address well-understood state-isolation problems specific to long-lived Swoole workers and do not alter any public API contract.

The coroutine state isolation, stream handler rewrites, enum identifier normalization, and queue payload serialization changes are all well-scoped and internally consistent. The recursion guard design is correct, the coroutine-ID check in isReporting() correctly prevents false positives from replicated child contexts, and the stack() channel-name fix is a genuine bug fix. No correctness defects were found in the changed code paths.

No files require special attention. The most complex new code is in PerformsSafeStreamOperations and FingersCrossedHandler; both follow the established CoroutineContext/ReplicableContext pattern consistently.

Important Files Changed

Filename Overview
src/log/src/Logger.php Replaces coroutine-keyed raw context with a typed LoggerState object; adds a coroutine-local recursion depth guard and disables Monolog's process-global loop detector. Logic is sound.
src/log/src/LogManager.php Splits get() into get()+createLogger(); fixes the stack() call to pass 'name' instead of 'channel' to createStackDriver; substitutes coroutine-safe handlers for Monolog originals; adopts RebindsCallbacksToSelf for extend().
src/log/src/Handlers/FingersCrossedHandler.php New coroutine-aware FingersCrossedHandler using a worker-unique static ID and CoroutineContext-backed FingersCrossedState for buffer and buffering flag.
src/log/src/Handlers/Concerns/PerformsSafeStreamOperations.php Replaces Monolog's set_error_handler/restore_error_handler pattern with try/catch Throwable; adds inode-change detection for external log rotation.
src/log/src/Handlers/RotatingFileHandler.php Overrides write() to use writeStreamSafely; eager rotation after first write on new files is intentional for long-lived workers.
src/log/src/Processors/UidProcessor.php Stores the per-request UID in CoroutineContext under a worker-unique static ID, preventing UID bleed across concurrent requests.
src/support/src/RebindsCallbacksToSelf.php New trait that rebinds anonymous closures to the manager instance while leaving static closures and first-class callables untouched, fixing the previous TypeError on static closure registration.
src/support/src/Manager.php Adds UnitEnum support to driver(); adopts RebindsCallbacksToSelf for extend(); treats empty string same as null for default driver resolution.
src/cache/src/Repository.php Replaces enum_value($key) with explicit UnitEnum instanceof checks and (string) casts across all key-accepting methods, preserving PSR-16 semantics.
src/queue/src/BackgroundQueue.php Serializes job payload at dispatch time via createPayload(), ensuring the executing coroutine observes the dispatched job and context snapshot.
src/queue/src/DeferredQueue.php Same payload-serialization-at-dispatch-time fix as BackgroundQueue.
src/foundation/src/Exceptions/Handler.php Tracks the currently-reporting exception in CoroutineContext with a coroutine-ID guard and restores previous reporting state in a finally block for nested calls.
src/log/src/Formatters/JsonFormatter.php New JsonFormatter enriching the serialized exception object with handler context, guarded by isReporting() to avoid re-entrant buildContextForException calls.
src/log/src/LoggerState.php ReplicableContext DTO for context array and recursion depth; replicate() copies context but resets depth.
src/log/src/Handlers/FingersCrossedState.php ReplicableContext DTO for fingers-crossed buffer/buffering flag; replicate() returns fresh empty state.

Reviews (2): Last reviewed commit: "Align Reverb presence integration assert..." | Re-trigger Greptile

Comment thread src/log/src/LogManager.php
Comment thread src/log/src/Formatters/JsonFormatter.php
Keep the existing coroutine-local logging depth active while MessageLogged events are dispatched so a same-channel listener that logs recursively reaches the established loop-abort threshold instead of exhausting the Swoole worker. Add a bounded regression that demonstrates the listener path emits the warning and terminates without introducing cross-channel coordination.

Document why active exception reports are not enriched a second time inside JsonFormatter, and record the deliberate restraint against generic cross-channel loop detection. On-demand logger construction remains uncached and unchanged.
Decode the nested member_removed payload and assert the normalized string presence identifier directly. Presence identifiers are intentionally normalized at the protocol boundary so zero remains valid; the integration test still expected the previous numeric JSON representation even though the focused Reverb regressions already covered the string contract.
@binaryfire

Copy link
Copy Markdown
Collaborator Author

@greptileai Thanks for the careful review. The follow-up is complete.

  • Extended the existing coroutine-local recursion guard through MessageLogged dispatch. Same-channel listeners that log recursively now reach the established warning/abort threshold instead of exhausting the Swoole worker. The regression is bounded and fails against the previous placement. Cross-channel coordination remains deliberately unsupported; adding shared loop state across independent channels would be disproportionate.
  • Kept Log::build() uncached. The old ondemand slot was undocumented worker-global state that could retain runtime configuration across requests and shadow a configured channel. There are no repository callers relying on retrieval through channel('ondemand'), and compatibility machinery would preserve the defect.
  • Documented why JsonFormatter skips rebuilding context for an exception already being reported: the context is present at record level, while rebuilding it can duplicate data and re-enter user callbacks.
  • Corrected the Reverb integration assertion exposed by CI. Presence IDs are intentionally normalized to strings so zero remains a valid identifier; the test now decodes the nested payload and verifies the string contract directly.

I also re-audited the logging design specifically for unnecessary complexity. The larger adapters exist only at exact Monolog boundaries whose process-global error handlers and worker-shared mutable state are unsafe under Swoole. No generic handler framework, mutex, pool, per-request logger rebuild, cross-channel registry, or speculative compatibility layer was added.

Focused logging and Reverb tests, formatting, both PHPStan configurations, diff checks, and the complete parallel suite are green.

@greptile-apps

greptile-apps Bot commented Jul 16, 2026

Copy link
Copy Markdown

The follow-up is thorough and the explanations hold up on re-examination.

On the build() / ondemand thread specifically: you're right to correct the failure mode I described. createLogger() catching construction failures and falling back to the emergency logger means the behavior for existing channel('ondemand') callers is a silent degradation to the emergency logger rather than a surfaced exception — which is actually a harder failure mode to diagnose than the one I named. The decision to leave it uncached is still correct given the worker-global leakage risk, but that nuance is worth capturing somewhere (upgrade notes or a method docblock) so teams with observability pipelines that depend on the fallback path can identify it.

The rest of the changes are clean:

  • Extending the recursion guard through MessageLogged dispatch is the right scope — cross-channel coordination would require shared mutable state that contradicts the isolation goal.
  • The JsonFormatter comment addition addresses the maintainability concern without changing behavior.
  • Normalizing Reverb presence IDs to strings and asserting the string contract directly is correct; zero as a valid presence identifier is a real case.

No further changes needed from my side. The design rationale is sound and the regression coverage matches the stated risk surface.

@binaryfire
binaryfire merged commit e56d998 into 0.4 Jul 16, 2026
36 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant