-
Notifications
You must be signed in to change notification settings - Fork 12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for alternate backends, including a NATS adapter #79
base: master
Are you sure you want to change the base?
Conversation
There was nothing there anyway
@@ -0,0 +1,48 @@ | |||
require "turbo/cable" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since Cable
adopted a bunch of the customizations I'd made to my fork, I didn't have to make a single change to my turbo/cable
integration! 💯
#{Turbo.javascript_tag} | ||
#{Turbo.cable_tag} | ||
#{Turbo::Frame.new(id: "time") { }} | ||
#{Turbo.stream_from "time"} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It blew my mind that this was all we needed to render in order to demo this. 🤯
def close_subscribe_connection | ||
nats.close rescue nil | ||
end | ||
|
||
def close_publish_connection | ||
nats.close rescue nil | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NATS multiplexes everything over a single TCP connection (no connection pool needed), so we ignore any errors caused by closing the client twice.
This might be too naive since NATS::Client#close
also drains subscriptions (processes any pending messages and synchronous requests), which can also raise errors, so we may want this to be a bit more precise on what exceptions it swallows silently.
getter streams = Hash(String, Set(NATS::Subscription)).new { |streams, channel| | ||
streams[channel] = Set(NATS::Subscription).new | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Had to use {}
instead of do
/end
here due to precedence.
The reason we had to include this is because you can't unsubscribe from a NATS subject directly. You have to unsubscribe from the subscription id since you can subscribe to the same subject multiple times.
register "redis" | ||
register "rediss" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Registering which URI schemes you support is as simple as this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🐍
setting redis_ping_interval : Time::Span do | ||
backend_ping_interval | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't tell if Habitat actually supports the block format like getter
does, but it didn't raise an error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh, that's interesting... At this level, setting
is a macro that takes a TypeDeclaration. That basically does this..
def self.{{ type_dec.var }} : {{ type_dec.type }}
{{ type_dec.value }}
end
A little more complex, but that basic idea. So I guess the question is, what does Crystal consider the block here? My guess is the block is passed to setting
which never calls yield
and so it's ignored 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, there's an open issue on it. luckyframework/habitat#54
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very likely. When I looked at the code for the setting
macro it looked like there was some indirection that I didn't know how to follow.
What I was aiming for here was something like the block for getter
and property
to lazily initialize the setting, so if you were using the Redis-specific setting name it would still work, but now that I write that out I'm thinking actually maybe we could just do methods for that instead of setting multiple ivars?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, methods may be fine here. I'm updating habitat issues to track some of these things too.
Cable.configure do |settings| | ||
settings.route = "/cable" # the URL your JS Client will connect | ||
# settings.url = "redis:///" | ||
# settings.url = ENV.fetch("NATS_URL", "nats:///") | ||
settings.url = ENV.fetch("CABLE_BACKEND_URL", "redis:///") | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Notice that in the settings for the demo we aren't actually setting backend_class
anywhere. Which backend is used is based entirely on url
.
@backend : BackendCore | ||
|
||
def initialize | ||
@backend = REGISTERED_BACKENDS[URI.parse(::Cable.settings.url).scheme].new |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the magic behind the automatic runtime backend selection.
TODO: I want to provide a better error than a plain-old KeyError
. I'm thinking maybe something like this:
raise Cable::UnknownBackend.new("Cannot find a backend for URI scheme #{scheme.inspect}. Did you require the adapter for it?")
setting redis_ping_interval : Time::Span = 15.seconds | ||
setting backend_class : Cable::BackendCore.class = Cable::RegistryBackend, example: "Cable::RedisBackend" | ||
setting backend_ping_interval : Time::Span = 15.seconds | ||
@[Deprecated("Use backend_ping_interval")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd be curious to see if this shows... Since the method doesn't take annotations in to consideration, this may not actually render. That would be a nice feature for Habitat though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you're right. It looks like it collects the settings
into a data structure and then emits them later instead of emitting them in-place, so this annotation may actually be attached to something else, which would be confusing, or nothing at all. 🤔
I love this update. This should also make it easier to add #49 too which I've been wanting to do. Looks like there's still a few things left to clean up before we go merging stuff, but it's a great start! |
Definitely. When I first looked at this I was trying to figure out how we'd make the backends pluggable and reduce dependency on Redis specifically. As an experiment*, I'm not using Redis at all in one of my services so I wanted to see if we could get Cable working with NATS so I wouldn't need to add it. 😄 * Trying to see if I could do all the things I typically use Redis for (sessions, caching, background jobs, and pub/sub) in NATS since it supports key/value storage as well as pub/sub and streams. So far, it's working really well. |
@jgaskins I've created a new shard for this https://github.com/cable-cr/cable-nats I didn't port any of your code over, but just started the initial shard structure. I hoping this also makes things easier to test. I'm gonna leave this PR open for now just so it's easier to find 😆 but I'll close it out eventually since we won't merge this in to this shard directly. |
I haven't added have any specs because this started as just a proof of concept, and I think I broke existing specs, but I did include an example app that lets you choose between backends by changing only the configuration.
The way it works is by changing the default backend to a
RegistryBackend
, and the individual backend implementations "register" themselves with it. Then when a client connects, it chooses the backend to use based on the URI scheme inCable.settings.url
. You can still setsettings.backend_class
to the specific implementation so it should remain backwards-compatible, but we couldn't leave the default asRedisBackend
since, in the case where the application doesn't use Redis, it wouldn't exist as a class and the app wouldn't compile. Since theRegistryBackend
references the other backends at connection time based on configuration, we don't rely on any specific implementation as the default.