Skip to content

Allow dynamically changing the channel in broadcast adapters #2

@d4rky-pl

Description

@d4rky-pl

Our application runs as two separate servers handling different traffic - one for the back office area and one for all our customers. As a result we're also running two separate AnyCable servers to allow separate scaling and to keep things walled. They still use the same database and Redis but they may run different Ruby code.

In some cases we want the customer-facing application to be able to trigger GraphQL subscriptions (using anycable-graphql) to the API sitting in the other realm, e.g. if customer sends a message to the ops, the ops application should refresh the chat and the other way around.

There's currently no way to change the AnyCable channel configuration on the fly for redis and redisx adapters (and most likely others too). It would be great to be able to change it, for ex. inside the scope of the block.

Considerations

Current code is not thread-safe - the broadcast adapter is configured in broadcast_adapter= of AnyCable singleton and the channel is set during that initialization, also as an @ivar (@channel). There are two ways this could be resolved to allow dynamic replacement - either with Thread.current with fallback to AnyCable.config.redis_channel (that's how our monkey-patch works but it's likely not ideal for all cases) or a mutex on changing the @channel value.

@palkan is this a feature you'd be interested in supporting? If so I can send a PR implementing it for all broadcast adapters.

The monkey-patch we're using
module AnyCableDynamicChannel
  def initialize(channel: AnyCable.config.redis_channel, **options)
    super
    @channel = nil
    Thread.current[:channel] = channel
  end

  def channel
    Thread.current[:channel] || AnyCable.config.redis_channel
  end

  def with_channel(channel_name)
    return yield if channel_name == channel

    begin
      old_channel = channel
      Thread.current[:channel] = channel_name
      yield
    ensure
      Thread.current[:channel] = old_channel
    end
  end

  def to_admin(&block)
    with_channel('__anycable-admin__', &block)
  end

  def to_www(&block)
    with_channel('__anycable-www__', &block)
  end
end

Usage in code:

def trigger_subscription(schema, event_name, args, object, scope: nil, context: nil)
  raise InvalidSchemaName, "Unknown schema #{schema}, expected one of #{SCHEMAS.keys.join(', ')}" unless SCHEMAS.key?(schema)

  if schema == :admin
    to_admin { SCHEMAS[schema].subscriptions.trigger(event_name, args, object, scope:, context:) }
  else
    to_www { SCHEMAS[schema].subscriptions.trigger(event_name, args, object, scope:, context:) }
  end
end

def to_admin(&block)
  ::AnyCable.broadcast_adapter.to_admin(&block)
end

def to_www(&block)
  ::AnyCable.broadcast_adapter.to_www(&block)
end

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions