Smart Eventing
View SourceAntenna is an elegant event handling framework that combines the best features of Phoenix.PubSub and gen_event with sophisticated pattern matching and back-pressure control. Built on GenStage and fully distributed by design, it offers a powerful yet simple way to handle events across your Elixir applications.
Key Features
1. Smart Pattern Matching
Unlike traditional pub/sub systems, Antenna provides Elixir-native pattern matching capabilities:
# Match on complex patterns with guards
Antenna.match(MyApp.Antenna,
{:temperature, celsius, location} when celsius > 30,
fn channel, event ->
Logger.warning("High temperature alert: #{inspect(event)} on #{channel}")
end,
channels: [:sensors])
2. Built for Distribution
- Automatic event propagation across connected nodes
- Node-local pattern matching for efficiency
- Built-in back-pressure through GenStage
- Process group-based channel management
# Events automatically propagate to all connected nodes
Node.connect(:"node2@host2")
Antenna.event(MyApp.Antenna, [:alerts], {:system_status, :degraded})
3. Flexible Event Handling
Support for both asynchronous and synchronous event processing:
# Async event dispatch
Antenna.event(MyApp.Antenna, [:logs], {:user_action, user_id, :login})
# Sync event with collected responses
responses = Antenna.sync_event(MyApp.Antenna, [:auth], {:verify_token, token})
4. Channel-based Organization
- Multiple isolated Antenna instances
- Channel-based event routing
- Support for wildcard subscriptions
- Dynamic channel subscription management
# Different antennas for different subsystems
Antenna.event(:metrics, [:system], %{cpu: 80, memory: 70})
Antenna.event(:business, [:orders], %{order_id: "123", status: :completed})
5. OTP-Compliant Design
- Follows OTP design principles
- Supervisor-based process management
- Clean process isolation
- Proper error handling and recovery
Real-world Applications
Event-Driven Microservices
# Service A: Order Processing
Antenna.match(OrderSystem,
%{event: :order_created, total: total} when total > 1000,
fn _, event ->
HighValueOrderProcessor.handle(event)
end,
channels: [:orders])
# Service B: Notification System
Antenna.match(NotificationSystem,
{:order_status, order_id, :completed},
fn _, event ->
CustomerNotifier.order_completed(order_id)
end,
channels: [:order_updates])
IoT Data Processing
# Device data handling with pattern matching
Antenna.match(IoTSystem,
{:sensor_data, device_id, readings} when readings.temperature > 90,
fn channel, event ->
DeviceAlertHandler.process_alert(channel, event)
end,
channels: [:device_telemetry])
Distributed Systems Monitoring
# System metrics collection
Antenna.match(Monitoring,
{:metric, node_name, metric, value} when value > threshold(),
fn _, event ->
MetricsAggregator.record_threshold_breach(event)
end,
channels: [:system_metrics])
Getting Started
- Add Antenna to your dependencies:
def deps do
[{:antenna, "~> 0.2"}]
end
- Add it to your supervision tree:
defmodule MyApp.Application do
use Application
def start(_type, _args) do
children = [
{Antenna, name: MyApp.EventSystem}
]
Supervisor.start_link(children, strategy: :one_for_one)
end
end
- Set up your event handlers:
# Define matchers for your events
Antenna.match(MyApp.EventSystem,
{:user_event, user_id, action},
fn channel, event ->
MyApp.EventTracker.track(channel, event)
end,
channels: [:user_activity])
# Start sending events
Antenna.event(MyApp.EventSystem, [:user_activity],
{:user_event, "user123", :login})
Why Choose Antenna?
- Native Pattern Matching: Leverage Elixir's powerful pattern matching for event handling
- Distributed by Design: Built for distributed systems with automatic node synchronization
- Back-pressure Control: Built on GenStage for controlled event flow
- Flexible Configuration: Multiple isolated instances and channel-based routing
- Production Ready: OTP-compliant with proper supervision and error handling
Internal Architecture
Event Flow
Antenna processes events through a distributed pipeline:
┌───────────────┐
│ │ Antenna.event(AntID, :tag1, %{foo: 42})
│ Broadcaster │───────────────────────────────────────────────
│ │
└───────────────┘
──/──
────── / ─────
────── / ──────
────── / ─────
──── / ───
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ │ │ │ │ │
│ Consumer@node1 │ │ Consumer@node1 │ … │ Consumer@nodeN │
│ │ │ │ │ │
└────────────────┘ └────────────────┘ └────────────────┘
· · ·
/ \ / \ / \
/ \ / \ / \
· mine? · · mine? · · mine? ·
\ / \ / \ /
\ / \ / \ /
· · ·
│ │ │
│ │ │
───── │ ─────
│
│
┌──────────────┐
│ │ if (match?), do: call_handlers(event)
│ matchers │───────────────────────────────────────────────────
│ │
└──────────────┘
Components
Broadcaster (GenStage Producer)
- Distributes events to all nodes
- Implements back-pressure control
- Handles sync/async event dispatch
Process Groups
- Channel-based process groups for event routing
- Dynamic subscriber management
- Node-local event matching
Pattern Matchers
- Handle complex pattern matching
- Support guards and conditions
- Route events to appropriate handlers
Guard
- Supervises channel subscriptions
- Manages handler lifecycle
- Ensures clean process termination
Back-pressure
Antenna uses GenStage to implement back-pressure:
- Events are dispatched based on consumer demand
- System stays responsive under load
- Memory usage remains stable
- Event ordering is preserved
Distribution
Built-in distribution capabilities:
- Automatic node discovery
- Transparent event propagation
- Node-local pattern matching
- Efficient event routing
For a detailed implementation guide and more examples, visit the official documentation.
Configuration
Antenna can be configured in your config.exs
:
config :antenna,
id: MyApp.Antenna, # Optional custom identifier
distributed: true, # Enable distributed mode (default: true)
sync_timeout: 5_000 # Default timeout for sync_event/4
Best Practices
1. Channel Organization
- Use atoms for channel names when possible
- Group related events under common channels
- Consider using hierarchical channel naming:
# Good channel organization Antenna.event(MyApp.Antenna, [:users_created], event) Antenna.event(MyApp.Antenna, [:users_updated], event)
2. Pattern Matching
- Use specific patterns to avoid unnecessary matches:
# Good - specific pattern Antenna.match(MyApp.Antenna, {:user_created, user_id, meta}, handler) # Less efficient - overly broad pattern Antenna.match(MyApp.Antenna, {_action, _id, _}, handler)
- Include guards for more precise matching
- Consider the order of pattern matches when using multiple matchers
3. Handler Design
- Keep handlers focused and single-purpose
- Use
sync_event/4
only when you need responses - Consider timeouts for sync operations:
# Set appropriate timeout for sync operations responses = Antenna.sync_event(MyApp.Antenna, [:auth], event, 10_000)
- Handle errors within handlers to prevent cascade failures
4. Performance
- Use async events (
event/3
) by default - Keep handler processing time minimal
- Consider using separate processes for long-running operations:
Antenna.match(MyApp.Antenna, event_pattern, fn channel, event -> # Spawn long-running operations Task.start(fn -> LongRunningProcessor.process(event) end) end)
- Monitor matcher and handler counts
5. Testing
- Test matchers with various event patterns:
test "matches high temperature events" do Antenna.match(TestAntenna, {:temperature, val, _} when val > 30, self(), channels: [:test]) Antenna.event(TestAntenna, [:test], {:temperature, 35, :room}) assert_receive {:antenna_event, :test, {:temperature, 35, :room}} end
- Test handler behavior with both valid and invalid events
- Test distributed scenarios
- Use ExUnit's
async: true
when possible
Conclusion
Antenna provides a robust foundation for building distributed event-driven systems in Elixir. Whether you're building microservices, processing IoT data, or managing system events, Antenna offers the tools you need with the elegance and reliability you expect from Elixir applications.