In roughly chronological order:
I’ve developed a real affection for Rubocop over the last couple of years. (Sorry to my old coworkers and friends at Planning Center, who put up with my complaining about it back then!) What I’ve come to appreciate is:
Promise.all(...) do
instead of Promise.all(...).then do
. The old code didn’t work at all. We added a Cop with an autocorrect
implementation, so we could upgrade any mistakes automatically!We have some GraphQL/GraphQL-Batch code for making authorization checks. It looks like this:
1 2 3 4 5 6 7 8 9 10 |
|
The authorized?
check returns a Promise
(for GraphQL-Batch), and inside that promise, .can_see?
returns true
or false
(synchronously).
However, to improve data access, we wanted to implement a new authorization code path:
1 2 |
|
This new code path would improve the database access under the hood to use our batch loading system.
After implementing the codepath, how could we update the ~1000 call sites to use the new method?
The easiest solution would be find-and-replace, but that doesn’t quite work because of boolean logic with Promises. Some of our authorization checks combined two checks like this:
1 2 |
|
If we updated that to async_can_see?
, that code would break. It would break because async_can_see?
always returns a Promise
, which is truthy. That is:
1
|
|
That code always returns true, even if one of the promises would resolve to false
. (The Ruby Promise
object is truthy, and we don’t have access to the returned value until we call promise.sync
.)
So, we have to figure out which code paths can be automatically upgraded.
Roughly, the answer is:
If an authorization returns the value of
.can_see?
, then we can replace that call with.async_can_see?
.
This is true because GraphQL-Ruby is happy to receive Promise<true|false>
– it will use its batching system to resolve it as late as possible.
So, how can we find cases when .can_see?
is used as a return value? There are roughly two possibilities:
return
s, which we don’t use oftenThis post covers that second case, implicit returns. We want to find implicit returns which are just calls to .can_see?
, and automatically upgrade them. (Some calls will be left over, we’ll upgrade those by hand.)
We assume that any code which is more complicated than just a call to .can_see?
can’t be migrated, because it might depend on the synchronous return of true|false
. We’ll revisit those by hand.
I knew I wanted two things:
async_can_see?
whenever possibleasync_can_see?
whenever it’s possibleRubocop will do both of these things:
def autocorrect
will fix existing violations, addressing the second goalBut it all depends on implementing the check well: can I find implicit returns? Fortunately, I only need to find them well enough: it doesn’t have to find every possible Ruby implicit return; it only has to find the ones actually used in the codebase!
By an approach of trial and error, here’s what I ended up with:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
|
With this cop, rubocop -a
will upgrade the easy cases in existing code, then I’ll track down the harder ones by hand.
I think the implementation could be improved by:
return
s. It wasn’t important for me because there weren’t any in this code base. next
Could probably be treated the same way, since it exists then
blocks..can_see?
, not only the easy ones. I expect that some usages are inevitable, but better to require a rubocop:disable
in that case to mark that it’s not best-practice.(Full disclosure: we haven’t shipped this refactor yet. But I enjoyed the work on it so far, so I thought I’d write up what I learned!)
]]>return
in one Ruby method could affect the flow of another method? I discovered it today while hunting a GraphQL-Ruby bugfix. You can get more reliable behavior with ensure
, if it’s appropriate.
Let’s imagine a simple instrumentation system, where method wraps a block of code and tags it with a name:
1 2 3 4 5 6 |
|
You could use this to instrument a method call, for example:
1 2 3 4 5 6 7 8 9 |
|
It prints the begin
message, then the end
message.
But what if you return early from the block? For example:
1 2 3 4 5 6 7 8 9 10 11 |
|
If you instrument it without returning from inside the block, it logs normally:
1 2 3 |
|
But, if you return early, you only get half the log:
1 2 |
|
Where’s the end
message?
Apparently, the return
inside the inner method (#do_stuff_with_instrumentation
) broke out of its own method and out of #instrument_event
. I don’t know why it works like that.
If you refactor the instrumentation to use ensure
, it won’t have this issue. Here’s the refactor:
1 2 3 4 5 6 |
|
Then, it prints normally:
1 2 3 |
|
Of course, this also changes the behavior of the method when errors happen. The ensure
code will be called even if yield
raises an error. So, it might not always be the right choice. (I bet you could use $!
to detect a currently-raised error, though.)
This recipe isn’t perfect: when using raw milk, a bit of the cream still separates to the top while it’s culturing. (I’d rather have it all mixed in, but I guess you could call it “cream top”!)
My sources are:
I’d like to add pictures someday, but for now, I recommend the pictures on Fankhauser’s blog.
Yogurt will only be as good as what you put in it. I generally sterilize everything I’ll need for the recipe:
To sterilize these items, I either:
Sometimes I forget an item at this step, in which case I wash it as welll as I can and hope for the best. I’ve never had anything really spoil, but I did have a batch that had a bit of kefir taste to it! I assume it had some yeast contamination. I still ate it 🤷.
Fankhauser recommends this step for pasteurization purposes, to kill unwanted bacteria. Brod & Taylor’s recipe prescribes a higher temperature (195°F) and a longer holding time in order to mess with the proteins and get a thicker yogurt. (I really don’t know how it accomplishes that. I read that it “denatures whey proteins”, and anyhow, I’m convinced because this is the same temperature you use to make whey ricotta, so there must be something to it.)
Anyway, first, heat the milk to 190°F and then take it off of the heat for 20 minutes.
The real trick is not to burn the milk. Whenever I cook it in a pot on the stove, I burn it on the bottom, no matter how much I use low heat and stir. Burning the milk is a double-whammy: it caramelizes some milk sugar, giving a hint of weird taste to the yogurt, and it sticks like cement to the bottom of the pot (adding insult to injury).
Finally, I found an approach that doesn’t burn the milk. I put a 2-gallon pot of milk inside my canner (without a lid) and fill the canner with water, then use it like a giant double-boiler. Just like a double-boiler, the hot water will buffer the temperature (it can’t go above boiling), and as a bonus, it heats the milk faster and more evenly, since the pot is surrounded with hot water. When the milk reaches the target temperature, I take it out of the double-boiler and set it aside.
The milk is now ready to become delicious yogurt, but it’s too hot for yogurt cultures to survive. Cool the milk to 120°F by placing it a sink of ice water. Stir frequently to equalize the temperature of the milk, and remove it when it reaches the target temperature.
Now, add the starter culture to the pot of milk. I’ve done it two ways:
Now that your yogurt is inoculated, distribute it into your jars (or whatever) to culture. I make 2 gallons at a time, so I use 8 quart jars.
Put your jars in a cooler, and fill the cooler with 120°F water. (Actually, I prep the cooler by adding some hot water first, then dumping it out and refilling this. I hope it warms up the cooler ahead-of-time.)
I used to really fuss with the water temperature, checking it from time to time and heating it back up to keep it close to 120°F. But then, I read in the Brod & Taylor recipe (linked above) that you might get a better texture by reducing the temperature after 1 hour.
I don’t follow that recipe, but I do close the cooler and forget about it. It cools off on its own and seems to turn out fine. I also read somewhere (don’t remember where?) that temperature variance helps complementary cultures (Bulgaricus and Thermophilus) get their own time to work on the milk. Something like, one of the bacteria does better on the higher range, while the other does better on the lower range.
I leave it for 8-16 hours, roughly all day or overnight. “Done” is a matter of taste. More culturing will make a tangier yogurt that separates whey more. Less time will make a sweeter yogurt with less whey separation.
I used to leave it for 24-ish hours, but I realized that the sweeter yogurt is tart enough and takes less time. Besides, I read on Brod & Taylor’s “How to Maintain a Yogurt Culture” that the culture will last longer if it has more lactose to digest while it’s in the fridge. I don’t follow all the steps in that article, but I hope it will keep my culture going strong for longer.
Dry off the jars and put them in the fridge. They’re sterilized and pasteurized, so besides the yogurt bacteria continuing to process the lactose, they don’t really spoil. I make yogurt once every month or two.
We end up eating yogurt as:
I’d like to try making frozen yogurt, but I haven’t yet!
]]>GraphQL::Execution::Interpreter
. It offers better performance and some new features.
In isolated benchmarks, the new runtime is about 50% faster. We saw about a 10% speedup in GitHub when we migrated.
You can opt in by adding to your schema:
1 2 3 4 5 6 |
|
But why rewrite?
Previously, each field evaluated by GraphQL-Ruby got its own instance of GraphQL::Query::Context::FieldResolutionContext
. This was introduced so that fields using graphql-batch
-style Promises could reliably access context values (like ctx.path
) after returning from the resolver (ie, when the promise was synced.)
The problem was, the bigger the response, the more ctx
objects would be created – and most of the time (for example, plain scalar fields), they were never used by application code. So, we allocated, initialized, then GCed these objects for nothing!
In fact, it wasn’t for nothing. As time passed, I started using those context objects inside execution code. For example, null propagation was implemented by climbing up the tree of context objects. So you couldn’t just stop creating them – the runtime depended on them.
To remove this performance issue, I went back to creating a single Query::Context
object and passing it to resolvers. If you’re using the new class-based API, you might have noticed that self.context
is a Query::Context
, not a Query::Context::FieldResolutionContext
. I did it this way to pave the way for removing this bottleneck.
But what about access to runtime information?
For fields that want runtime info (like path
or ast_node
), they can opt into it with extras: [...]
, for example:
1
|
|
By adding that configuration, the requested value will be injected into the resolver:
1 2 3 |
|
path
will be a frozen Array describing the current point in the GraphQL response.
Finally, since FieldResolutionContext
s aren’t necessary for user code, we can rewrite execution to not create or use them anymore. Under the hood, GraphQL::Execution::Interpreter
doesn’t create those ctx
objects. Instead, null propagation is implemented manually and all necessary values are passed from method to method.
Years ago, someone requested the feature of rejecting a query before running it. They wanted to analyze the incoming query, and if it was too big or too complicated, reject it.
How could this be implemented? You could provide user access to the AST, but that would leave some difficult processing to user code, for example, merging fragments on interfaces.
So, I added GraphQL::InternalRepresentation
as a normalized, pre-processed query structure. Before running a query, the AST was transformed into a tree of irep_node
s. Users could analyze that structure and reject queries if desired.
In execution code, why throw away the result of that preprocessing? The runtime also used irep_node
s to save re-calculating fragment merging.
In fact, even static validation used the irep_node
tree. At some point, rather than re-implement fragment merging, I decided to hook into that rewritten tree to implement FragmentsWillMerge
. After all, why throw away that work?
(As it turns out, someone should fire the GraphQL-Ruby maintainer. These layers of code were not well-isolated!!)
irep_node
s was slow and often a wasteSince the irep_node
tree was built for analysis, it generated branches for every possible combination of interfaces, objects, and unions. This meant that, even for a query returning very simple data, the pre-processing step might be very complex.
To make matters worse, the complexity of this preprocessing would grow as the schema grew. The more implementers an interface has, the longer it takes to calculate the possible branches in a fragment.
Not only was the work complex, but it also couldn’t be cached. This is because, while building the irep_node
tree, @skip
and @include
would be evaluated with the current query variables. If nodes were skipped, they were left out of the irep_node
tree.
This means that, for the same query in your code base, you couldn’t reuse the irep_node
tree, since the values for those query variables might be different from one execution to the next. Boo, hiss!
I want to empower people to use GraphQL-Ruby in creative ways, but throwing a wacky, custom data structure in the mix doesn’t make it easy. I think an easier execution model will encourage people to learn how it works and build cool new stuff!
The new runtime evaluates the AST directly. Runtime features (@skip
and @include
, for example) are implemented at, well, runtime!
Since you can’t use the irep_node
tree for analysis anymore, the library includes a new module, GraphQL::Analysis::AST
, for preprocessing queries. Shout out to @xuorig for this module!
For GitHub, we moved a lot of analyzer behavior to runtime. We did this because it’s easier to maintain and requires less GraphQL-specific knowledge to understand and modify. Although the client experience is slightly different, it’s still good.
For example, we had an analyzer to check that pagination parameters (eg first
and last
) were valid. We moved this to runtime, adding it to our connection tooling.
GraphQL::Execution::Lookahead
irep_node
s were useful for looking ahead in a query to see what fields would be selected next. (Honestly, they weren’t that good, but they were the only thing we had, beside using the AST directly).
To support that use, we now have extras: [:lookahead]
which will inject an instance of GraphQL::Execution::Lookahead
, with an API explicitly for checking fields later in the query.
As part of the change with removing FieldResolutionContext
, the new runtime doesn’t support proc-style resolvers ->(obj, args, ctx) {...}
. Besides ctx
, the args
objects (GraphQL::Query::Arguments
) are not created by the interpreter either. Instead, the interpreter uses plain hashes.
Instead of procs, methods on Object type classes should be used.
This means that proc-based features are also not supported. Field instrumenters and middlewares won’t be called; a new feature called field extensions should be used instead.
.to_graphql
is almost outWhen the class-based schema API was added to GraphQL-Ruby, there was a little problem. The class-based API was great for developers, but the execution API expected legacy-style objects. The bridge was crossed via a compatibility layer: each type class had a def self.to_graphql
method which returned a legacy-style object based on that class. Internally, the class and legacy object were cached together.
The interpreter doesn’t use those legacy objects, only classes. So, any type extensions that you’ve built will have to be supported on those classes.
The catch is, I’m not 100% sure that uses of legacy objects have all been migrated. In GitHub, we transitioned by delegating methods from the legacy objects to their source classes, and I haven’t removed those delegations yet. So, there might still be uses of legacy objects 😅.
In a future version, I want to remove the use of those objects completely!
I hope this post has clarified some of the goals and approaches toward adding the new runtime. I’m already building new features for it, like custom directives and better subscription support. If you have a question or concern, please open an issue to discuss!
]]>Schumacher was a post-WWII British economist. He advised the British National Coal Board (the nationalized coal company) and rebuilding of postwar Germany.
The book was published in 1973, the same year as the oil crisis, which raised some questions about our dependence on imported petroleum.
Schumacher returns again and again to a quote from John Maynard Keynes):
For at least another hundred years we must pretend to ourselves and to everyone that fair is foul and foul is fair; for foul is useful and fair is not. Avarice and usury and precaution must be our gods for a little longer still.
In context, Keynes is claiming that eventually, people will recognize will give up a love of money because they find that it doesn’t satisfy. But first, a capitalist order must create material abundance.
For Schumacher, this triggers a series of conclusions:
As a modern person, I ask myself, are we arriving at Keynes’s expected conclusion? He wrote:
When the accumulation of wealth is no longer of high social importance, there will be great changes in the code of morals.
I wonder when that “When…” is/was expected to arrive.
Schumacher’s understanding of social malaise and environmental destruction boils down to a claim about organizational structure, something like: when an enterprise becomes so big that ownership becomes isolated from execution, it becomes inhuman (in the sense that decision-making can’t be made with human-to-human perspective), and as a result, workers are reduced to cogs, natural resources are reduced to consumable inputs, and and the like.
Schumacher assumes (observes?) that when humans make decisions regarding their neighbors and hometowns, they are more likely to consider the non-economic factors of their decisions. For example, a non-economic factor might be a beautiful landscape, a creatively engaging endeavor, or caring for something (or someone) who can’t care for itself. For a profit-oriented calculation at headquarters, these factors might be weighed less heavily.
To simplify one of Schumacher’s maxims to address this issue, he suggests that nothing should be centralized if it can be decentralized. He acknowledges that some order is required in order for our larger societal goals to be accomplished, but also, he warns that too much order stunts many non-quantifiable joys in life. So, by decentralizing, you can engage human entrepreneurial spirit in a way that is healthy for the localities it impacts.
A large section of the book (“The Third World”) addresses development in poor countries. Schumacher criticizes the dominant mode of development, namely, the installation of capital-intensive heavy industries in large cities. He cites several problems:
Schumacher espouses a different approach, “intermediate technology”. In this approach, gradual improvements in technology are applied to slowly raise the level of economic activity in a community. For example, if human labor is readily available, a more low-capital, labor-intensive solution might be preferred to a high-capital, low-labor solution. For example, consumer goods might be made by hand instead of by machine, since that will employ more local people and can adapt to a greater variety of inputs. Additionally, several enterprises should be fostered, and they should target local markets, so that newly-employed people can participate in commerce with one another.
I have left out specific examples; the book is full of them.
You can see how this hangs on Schumacher’s conviction that work can be good for people. In “Buddhist Economics”, he highlights some possible benefits of employment, for example:
All these goods focus on the people involved in the enterprise, not the capital or products. Capital should be used in service to the people, not people in service to capital.
In “The Greatest Resource - Education”, Schumacher points out the any economic theory rests on a “meta-economics”, that is, a set of assumptions about what things are and what they mean. He describes it as
ideas that would make the world, and [our] own lives, intelligible to [us]; when a thing is intelligible, you have a sense of participation; when a thing is unintelligible, you have a sense of estrangement.
Importantly, these things are absorbed and transmitted without our active recognition of it. Our minds are “furnished” by our communities without our awareness (much less our permission!).
When it comes to our own “meta-economics”, outlines several of our assumptions about the world:
For Schumacher, since these ideas give rise to our economics, they can also be understood as the source of our social ills. Since they’re responsible for our sense of powerlessness, our quickness to consume and destroy the earth, and our dissatisfaction with these things, we can’t expect more of the same to remedy those ills.
Schumacher also prescribes a suite of assumptions which he think will give rise to the kind of economics he espouses:
Schumacher points to examples of these assumptions in several philosophical traditions outside post-Enlightenment Western thought.
(This post sat as a draft for a long time. I filled in the last part after returning the book.)
At the end of the book, Schumacher provides a positive theory of large organizations. Since I’ve returned the book, I’ll write the maxim that really stuck with me: anything that can be decentralized should be decentralized.
Toward the end of engaging peoples’ entrepreneurial spirit, underlings should be given as much autonomy and authority as possible. Besides doing better work, the boots-on-the-ground folks can make more appropriate consideration of local context.
Schumacher also imagined a really interesting relationship between government and big business. He pointed out that the current arrangement of taxation creates some bizarre incentives: since only profit is taxed, companies work to hide profit from the government. It’s impractical that the governemnt builds the infrastructure for businesses to thrive, then puts itself in the position of a bandit, trying to recapture (via taxation) its fair share of the gain.
What if, instead, big companies had the local government as a 50% shareholder? Shumacher proposes that the government party would be observer-only, except in circumstances when the local common good would require some representation. But, the government party would be entitled to 50% of the profit instead of taxing the business. In an arrangement like that, the business is incentivized to grow its profits, and the government doesn’t have to fight to recapture its investment in business infrastructure.
I don’t have any experience in that kind of large-scale thinking, but I found it an interesting scenario to imagine.
Interestingly, Schumacher clearly builds his vision on a Christian understanding of humans and work. He sees humans as reflecting their creator’s nature: creative, social, loving, and capable. At its best, work is not a necessary evil, but instead, it’s a good part of culture where people can engage those attributes. That perspective orients his thoughts towards goals other than “putting food on the table” (although sustenance is a goal), for example, engendering pride in one’s work, connections between neighbors, and the development and exercise of human skill.
]]>TL;DR: I applied a thing I read in a textbook and it:
You can see the diff and benchmark results here: https://github.com/rmosolgo/graphql-ruby/compare/1b306fad…eef73b1
It’s a bit funny, but it’s not totally clear to me what the book is trying to get at here. In the book, they talk about control context or continuations in a way that I would talk about “stack frames”. I think the problem is this: when you implement a programming language as an interpreter, you end up with recursive method calls, and that recursion builds up a big stack in the host language. This is bad because it hogs memory.
I can definitely imagine that this is a problem in Ruby, although I haven’t measured it. GraphQL-Ruby uses recursion to execute GraphQL queries, and I can imagine that those recursive backtrace frames hog memory for a couple reasons:
binding
), which, since it’s still on the stack, can’t be GCed. So, Ruby holds on to a lot of objects which could be garbaged collected if the library was written better.Besides that, the long backtrace adds a lot of noise when debugging.
In the book, they say, “move your recursive calls to tail position, then, assuming your language has tail-call optimization, you won’t have this problem.” Well, my language doesn’t have tail-call optimization, so I do have this problem! (Ok, it’s an option.)
Luckily for me, they describe a technique for solving the problem without tail-call optimization. It’s called trampolining, and it works roughly like this:
When a method would make a recursive call, instead, return a
Bounce
. Then, the top-level method, which previously received theFinalValue
of the interpreter’s work, should be extended to accept either aFinalValue
or aBounce
. In the case of aFinalValue
, it returns the value as previously. In the case of aBounce
, it re-enters the interpreter using the “bounced” value.
Using this technique, a previously-recursive method now returns, giving the caller some information about how to take the next step.
Let’s give it a try.
I want to test impact in two ways: memory consumption and backtrace size. I want to measure these values during GraphQL execution, so what better way to do it but build a GraphQL schema!
You can see the whole benchmark, but in short, we’ll run a deeply-nested query, and at the deepest point, measure the backtrace size and the number of live objects in the heap:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Where the fields are implemented by:
1 2 3 4 5 6 7 8 9 10 11 |
|
We’ll use these measurements to assess the impact of the refactor.
To begin with, the interpreter is implemented as a set of recursive methods. The methods do things like:
These methods are recursive in the case of fields that return GraphQL objects. The first method resolves a field and calls the second method; then the second method, in order to prepare an object as a GraphQL response, calls back to the first method, to resolve selections on that object. For example, execution might work like this:
Do you see how the same procedure is being applied over and over, in a nested way? That’s implemented with recursive calls in GraphQL-Ruby.
We can run our test to see how the Ruby execution context looks in this case:
1 2 3 |
|
This is the baseline for backtrace size and object count, which we’re using to measure memory overhead in GraphQL execution. (This describes behavior at this commit.)
As a requirement for the final refactor, we have to do some code reorganization. In the current code, the recursive calls require some setup and teardown around them. For example, we track the GraphQL “path”, which is the list of fields that describe where we are in the response. Here’s a field with its “path”:
1 2 3 4 5 6 7 |
|
In the code, it looks something like this:
1 2 3 4 5 6 |
|
The problem is, if I want to refactor execute_recursively
to become a Bounce
, it won’t do me any good, because the value of execute_recursively
isn’t returned from the method. It’s not the last call in the method, so its value isn’t returned. Instead, the value of @path.pop
is returned. (It’s not used for anything.)
This is to say: @path.pop
is in tail position, the last call in the method. But I want execute_recursively
to be in tail position.
The easiest way to “fix” that would be to refactor the method to return the value of execute_recursively
:
1 2 3 4 5 6 7 8 |
|
The problem is, when execute_recursively
is refactored to be a Bounce
:
1 2 3 4 5 6 7 8 |
|
By the time the bounce
is actually executed, path
won’t have the changes I need in it. The value is pushed and popped before the bounce is actually called.
The solution is to remove the need for @path.pop
. This can be done by creating a new path and passing it as input.
1 2 3 4 |
|
Now, execute_recursively
is in tail position!
(The actual refactor is here: https://github.com/rmosolgo/graphql-ruby/commit/ef6e94283ecf280b14fe5417a4ee6896a06ebe69)
Now, we want to replace recursive calls with a bounce, where a bounce is an object with enough information to continue execution at a later point in time.
Since my recursive interpreter is implemented with a bunch of stateless methods (they’re stateless since the refactor above), I can create a Bounce class that will continue by calling the same method:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Then, I replace the tail-position recursive calls with bounces:
1 2 |
|
Instead of growing the backtrace by calling another method, we’ll be shrinking the backtrace by returning from the current method with a Bounce.
You can see the refactor here: https://github.com/rmosolgo/graphql-ruby/commit/b8e51573652b736d67235080e8b450d6fc9cc92e
Let’s run the test:
1 2 3 |
|
It’s a success! The backtraceSize
decreased from 282 to 55. The objectCount
decreased from 812
to 686
.
“Trampolining” is the process of taking each bounce and continuing it. In my first implementation, def trampoline
looked like this:
1 2 3 4 5 6 7 8 9 10 11 |
|
My test indicated no improvement in memory overhead, so I frustratedly called it quits. While brushing my teeth before bed, it hit me! I had unwittingly re-introduced recursive method calls. So, I hurried downstairs and reimplemented def trampoline
to use a while
loop and a buffer of bounces, an approach which didn’t grow the Ruby execution context. Then the test result was much better.
Another consideration is the overhead of Bounces themselves. My first implementation creates a bounce before resolving each field. For very large responses, this will add a lot of overhead, especially when the field is a simple leaf value. This should be improved somehow.
It turns out that visitors to the website don’t care about backtrace size or Ruby heap size, they just care about waiting for webpages to load. Lucky for me, my benchmark includes some runtime measurements, and the results were basically the same:
1 2 3 4 5 6 |
|
The runtime performance was very similar, almost within the margin of error. However, the consideration of Bounce overhead described above could cause worse performance in some cases.
This code isn’t quite ready for GraphQL-Ruby, but I think it’s promising for a few reasons:
However, one serious issue still needs to be addressed: what about the Bounce
’s own overhead? Allocating a new object for every field execution is already a performance issue in GraphQL-Ruby, and I’m trying hard to remove it. So the implementation will need to be more subtle in that regard.
Here are a few thoughts about the trip.
Balkan Ruby was a big hit. Personally, some of my favorites were:
One of my favorite parts of the conference was the code challenges set up by Receipt Bank, one of the sponsors.
Every few hours, a new, wacky challenge would go live. Although I didn’t do well on them, I enjoyed working with a few new friends on different solutions, and seeing the creative things that other attendees submitted!
Sofia was great. A beautiful city with interesting architecture, tons of trees and tasty food.
The Nevski Cathedral was built in the early 1900s to celebrate Russia’s liberation of Bulgaria from the Ottomans:
Inside, a mural of Abraham and Isaac:
And St. Cyril and St. Methodius, creators of the Cyrillic alphabet, who are quite popular around here:
I really enjoyed the different cathedrals. There’s something cool about the different instructive artwork and “sacred” feeling of a beautiful building with incense burning. I wonder if modern American churches could do more to engage all of our senses.
Also, a bit of Soviet history found in a nearby park:
And here, some recently excavated Roman ruins, and the one remaining Turkish mosque downtown:
The mosque was right beside ruins of an old bathhouse. Apparently that’s why Sofia was founded here – there were hot springs on the road between Rome and Constantinople, so the Romans set up camp (and called it Serdica).
And a fairly typical meal during my time there, a shopska salad (veggies with cheese, oil, and vinegar):
I can’t say enough good things about the local dairy products. The cheese was soft and fresh and the yogurt was tart and refreshing.
My favorite part about programming conferences is meeting the smart, caring folks who make them possible, and Balkan Ruby was no exception.
The two main organizers, Genadi and Vestimir were fantastic hosts (and experienced, since they got their start with Euruko a few years back). Besides that, I really enjoyed meeting the volunteers and learning a bit about life in Sofia.
One thing that stood out to me was the tradition behind the local liquor, rakia. It turns out that many families make it themselves, despite a law against owning stills. I’ve been reading that peach wine was the traditional alcoholic drink for the earliest European arrivals to my area, so I decided to give it a shot this summer!
A big bonus was when Vestimir played trail guide for our hike up the nearby mountain, Mt. Vitosha. It turned out to be a gray day, but we had a blast anyways.
Some pictures of the trail:
Beautiful! But you could say were were a bit underdressed XD
At the summit, we were happy to find a lodge where some food was served.
Some traditional bean soup, bread, lyutenitsa, cheese tea, and rakia never tasted so good.
Enjoying a rest:
(Left-to-right: Andreas, Nynne, Sameer, Me and Vestimir)
And, pleasantly, we caught a nice view of Sofia on the way back down:
(You can even see the Nevski cathedral if you look closely!)
Balkan Ruby was a big hit on all fronts: great people, great city, great technical content. Especially as a dairy lover, I’ll take the next chance I get to go back! And I loved making some new friends, who I hope to see at future Ruby events.
]]>Here are the different variables in Ruby:
1 2 3 4 5 6 7 8 9 10 |
|
Here is how Ripper parses the above code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Let’s check out those nodes.
1 2 |
|
A :vcall
is a bareword, either a local variable lookup or a method call on self. Used alone, this can only be determined at runtime, depending on the binding. If there’s a local variable, it will be used. My guess is that :vcall
is short for “variable/call”
Interestingly, there is a single-expression case which could be disambiguated statically, but Ripper still uses :vcall
:
1 2 3 4 5 6 |
|
1 2 3 4 5 6 7 8 |
|
:var_ref
(presumably “variable reference”) is shared by many of these examples, and can always be resolved to a variable lookup, never a method call.
Its argument tells what kind of lookup to do (global, constant, instance, class), and what name to look up.
Some Ruby can be statically known to be a method call, not a variable lookup:
1 2 3 4 5 6 7 8 9 10 |
|
In these cases, :fcall
, :call
and :command
are used to represent definite method sends.
Interestingly, :var_ref
is used for self
, too.
If you want to know more about the motivations behind this work, check out this previous post.
Below, I’ll cover:
GitHub’s type definitions are separated into folders by type, for example: objects/
, unions/
, enums/
(and mutations/
). I worked through them one folder at a time. The objects/
folder was big, so I did it twenty or thirty files at a time.
I had to do interfaces/
last because of the nature of the new class-based schema. Interfaces modules’ methods can’t be added to legacy-style GraphQL object types. So, by doing interfaces last, I didn’t have to worry about this compatibility issue.
Now that I remember it, I did the schema first, and by hand. It was a pretty easy upgrade.
When I started each section, I created a base class by hand. (There is some automated support for this, but I didn’t use it.) Then, I ran the upgrader on some files and tried to run the test suite. There were usually two kinds of errors:
More on these errors below.
After upgrading a section of the schema, I opened a PR for review from the team. This was crucial: since I was working at such a large scale, it was easy for me to miss the trees for the forest. My teammates caught a lot of things during the process!
After a review, the PR would be merged into master. Since GraphQL 1.8.0 supports incremental migration, I could work through the code in chunks without a long running branch or feature flags.
Here’s an overview of how the upgrader works. After reading the overview, if you want some specific examples, check out the source code.
The gem includes an auto-upgrader, spearheaded by the folks at HackerOne and refined during my use of it. It’s encapsulated in a class, GraphQL::Upgrader::Member
.
To use the upgrader, I added a Ruby script to the code base called graphql-update.rb
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
|
This script has two basic parts:
GraphQL::Upgrader::Member
with a set of custom transformationsIn your own script, you can write whatever supporting code you want. The key part from GraphQL-Ruby is:
1 2 3 4 |
|
The upgrader is structured as a pipeline: each step accepts a big string of input and returns a big string of output. Sometimes, a step does nothing and so its returned string is the same as the input string. In general, the transforms consist of two steps:
parser
gem.)You have a few options for customizing the transformation pipeline:
(The “pipeline” is just an array of instances or subclasses of GraphQL::Upgrader::Transform
.)
We’ll see cases of each below.
The upgrader accepts several types of transform pipelines:
1 2 3 4 5 6 |
|
type_transforms
are run first, on the entire file.field_transforms
are run second, but they receive parts of the type definition. They receive calls to field
, connection
, return_field
, input_field
, and argument
. Fine-grained changes to field definition or argument definition go here.clean_up_transforms
are run last, on the entire file. For example, there’s a built-in RemoveExcessWhitespaceTransform
which cleans up trailing spaces after other transforms have run.skip:
has a special function: its #skip?(input)
method is called and if it returns true, the text is not transformed at all. This allows the transformer to be idempotent: by default, if you run it on the same file over and over, it will update the file only once.Here are some custom transforms applied to our codebase.
We had a wrapper around ObjectType.define
which attached metadata, linking the object type to a specific Rails model. The helper was called define_active_record_type
. I wanted to take this:
1 2 3 4 5 6 7 |
|
And make it this:
1 2 3 4 5 6 7 8 |
|
Fortunately, this can be done with a pretty straightforward regular expression substitution. Here’s the transform:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Then, in graphql-update.rb
, this transform was put first in the list:
1 2 3 |
|
Also, for this to work, I added the def self.model_name(name)
helper to the base class.
We have a helper for adding URL fields called define_url_field
. I decided to rename this to url_fields
, since these days it creates two fields.
The arguments are the same, so it was a simple substitution:
1 2 3 4 5 6 7 |
|
This transform didn’t interact with any other transforms, so I added it to clean_up_transforms
, so it would run last:
1 2 3 4 |
|
We have a few DSL methods that, at the time, were easier to implement as keyword arguments. (Since then, the API has changed a bit. You can implement DSL methods on your fields by extending GraphQL::Schema::Field
and setting that class as field_class
on your base Object, Interface and Mutation classes.)
I wanted to transform:
1 2 3 |
|
To:
1
|
|
(Later, a built-in upgrader would change secretStuff
to secret_stuff
and types.String
to String, null: true
.)
To accomplish this, I reused a built-in transform, ConfigurationToKwargTransform
, adding it to field_transforms
:
1 2 3 4 |
|
In fact, there were several configuration methods moved this way.
As I was working through the code, some files were tougher than others. So, I decided to skip them. I decided that a magic comment:
1
|
|
would cause a file to be skipped. To implement this, I made a custom skip class:
1 2 3 4 5 |
|
And passed it as skip:
to the upgrader. Then, later, I removed the comment and tried again. (Fortunately, my procrastination paid off because the upgrader was improved in the meantime!)
As I worked, I improved the upgrader to cover as many cases as I could, but there are still a few cases that I had to upgrade by hand. I’ll list them here. If you’re really dragged down by them, consider opening an issue on GraphQL-Ruby to talk about fixing them. I’m sure they can be fixed, I just didn’t get to it!
If you want to fix one of these issues, try to replicate the issue by adding to an example spec/fixtures/upgrader
and then getting a failing test. Then, you could update the upgrader code to fix that broken test.
Arguments could be accessed by method to avoid typos. However, now, since arguments are a Ruby keyword hash, they don’t have methods corresponding to their keys.
Unfortunately, the upgrader doesn’t do anything about this, it just leaves them there and you get a NoMethodError
on Hash
.
This could almost certainly be fixed by improving this find-and-replace in ResolveProcToMethodTransform
:
1 2 3 4 5 |
|
It only updates a few methods on args
, but I bet a similar find-and-replace could replace other method calls, too.
Sometimes, we take GraphQL arguments and pass them to helper methods:
1 2 3 |
|
However when this was transformed to:
1 2 3 |
|
It would break, because the new arguments
value is a Ruby hash with underscored, symbol keys. So, if Some::Helper
was using camelized strings to get values, it would stop working.
The upgrader can’t really do anything there, since it’s not analyzing the codebase. In my case, these were readily apparent because of failing tests, so I went and fixed them.
We have some fields that add to the "errors"
key and return values, they used ctx.add_error
to do so:
1 2 3 4 5 6 7 8 |
|
When upgraded, it doesn’t work quite right:
1 2 3 4 5 6 7 8 |
|
(If you don’t have to return a value, use raise
instead, then you can stop reading this part!)
The problem is that @context
is not a field-specific context anymore. Instead, it’s the query-level context. (This is downside of the new API: we don’t have a great way to pass in the field context anymore.)
To address this kind of issues, field
accepts a keyword called extras:
, which contains a array of symbols. In the case above, we could use :execution_errors
:
1 2 3 4 5 6 7 |
|
So, execution_errors
was injected into the field as a keyword. It is field-level, so adding errors there works as before.
Other extras are :irep_node
, :parent
, :ast_node
, and :arguments
. It’s a bit of a hack, but we need something for this!
By default, connection arguments (like first
, after
, last
, before
) are not passed to the Ruby methods for implementing fields. This is because they’re generally used by the automagical (😖) connection wrappers, not the resolve functions.
But, sometimes you just need those old arguments!
If you use extras: [:arguments]
, the legacy-style arguments will be injected as a keyword:
1 2 3 4 5 6 |
|
The upgrader does fine when the description is a "..."
or '...'
string. But in other cases, it was a bit wacky.
Strings built up with +
or \
always broke. I had to go back by hand and join them into one string.
Heredoc strings often worked, but only by chance. For example:
1 2 3 4 5 |
|
Would be transformed to:
1 2 3 |
|
This is valid Ruby, but a bit tricky. This could definitely be improved: since I started my project, GraphQL 1.8 was extended to support description
as a method as well as a keyword. So, the upgrader could be improved to leave descriptions in place if they’re fancy strings.
I hacked around with the parser
gem to transform resolve
procs into instance methods, but there’s a bug. A proc like this:
1 2 3 4 |
|
Will be transformed to:
1 2 3 |
|
Did you see how the comment was removed? I think I’ve somehow wrongly detected the start of the proc body, so that the comment was left out.
In my case, I re-added those comments by hand. But it could probably be fixed in GraphQL::Upgrader::ResolveProcToMethodTransform
.
I’m not sure why, but sometimes a hash of arguments like:
1 2 3 4 5 6 |
|
would be reorganized to
1 2 3 4 |
|
I have no idea why, and I didn’t look into it, I just fixed it by hand.
We have a DSL for making connections, like:
1
|
|
Sometimes, when this connection was inside a proc, it would be wrongly transformed to:
1
|
|
This was invalid Ruby, so the app wouldn’t boot, and I would fix it by hand.
Generating connection and edge types with the .connection_type
/.define_connection
and .edge_type
/.define_edge
methods will work fine with the new API, but if you want to migrate them to classes, you can do it.
It’s on my radar because I want to remove our DSL extensions, and that requires updating our custom connection edge types.
Long story, short, it Just Work™ed with the class-based API. The approach was:
BaseObject
def self.inherited
hook to add connection- and edge-related behaviorsSo, I will share my base classes in case that helps. Sometime it will be nice to upstream this to GraphQL-Ruby, but I’m not sure how to do it now.
Base connection class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
|
Base edge class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
We have several extensions to the GraphQL-Ruby .define
DSL, for example, visibility
controls who can see certain types and fields and scopes
maps OAuth scopes to GraphQL types.
The difficulty in porting extensions comes from the implementation details of the new API. For now, definition classes are factories for legacy-style type instances. Each class has a .to_graphql
method which is called once to return a legacy-style definition. To maintain compatibility, you have to either:
Eventually, legacy-style definitions will be phased out of GraphQL-Ruby, but for now, they both exist in this way in order to maintain backwards compatibility and gradual adoptability.
In the mean time, you can go between class-based and legacy-style definitions using .graphql_defintion
and .metadata[:type_class]
, for example:
1 2 3 4 5 6 7 8 |
|
.redefine
The easiest way to retain compatibility is to:
.to_graphql
to call super, and then pass the configuration to defn.redefine(...)
, then return the redefined type.After my work on our code, I extracted this into a backport of accepts_definition
You can take that approach for a try, for example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
.metadata[:type_class]
An approach I haven’t tried yet, but I will soon, is to move the “source of truth” to the the class-based definition. The challenge here is that class-based definitions are not really used during validation and execution, so how can you reach configuration values on those classes?
The answer is that if a legacy-style type was derived from a class, that class is stored as metadata[:type_class]
. For example:
1 2 3 4 5 |
|
So, you could update runtime code to read configurations from type_defn.metadata[:type_class]
.
Importantly, metadata[:type_class]
will be nil
if the type wasn’t derived from a class, so this approach is tough to use if some definitions are still using the .define
API.
I haven’t implemented this yet, but I will be doing it in the next few weeks so we can simplify our extensions and improve boot time.
I’m still wrapping up some loose ends in the codebase, but I thought I’d share these notes in case they help you in your upgrade. If you run into trouble on anything mentioned here, please open an issue on GraphQL-Ruby! I really want to support a smooth transition to this new API.
]]>1.8.0
will have a new class-based API for defining your schema. Let’s investigate the design choices in the new API.
The new API is backwards-compatible and can coexist with type definitions in the old format. See the docs for details. 1.8.0.pre
versions are available on RubyGems now and are very stable – that’s what we’re running at GitHub!
Since starting at GitHub last May, I’ve entered into the experience of a huge-scale GraphQL system. Huge scale in lots of ways: huge schema, huge volume, and huge developer base. One of the problems that stood out to me (and to lots of us) was that GraphQL-Ruby simply didn’t help us be productive. Elements of schema definition hindered us rather than helped us.
So, our team set out on remaking the GraphQL-Ruby schema definition API. We wanted to address a few specific issues:
graphql-js
, the reference implementation.) Ruby developers couldn’t bring their usual practices into schema development; instead, they had to learn a bunch of new APIs and figure out how to work them together.Besides all that, we needed a safe transition, so it had to support a gradual adoption.
After trying a few different possibilities, the team decided to take a class-based approach to defining GraphQL schemas. I’m really thankful for their support in the design process, and I’m indebted to the folks at Shopify, who used a class-based schema definition system from the start (as a layer on top of GraphQL-Ruby) and presented their work early on.
In short, GraphQL types used to be singleton instances, built with a block-based API:
1 2 3 |
|
Now, GraphQL types are classes, with a DSL implemented as class methods:
1 2 3 |
|
Field resolution was previously defined using Proc literals:
1 2 3 4 5 6 |
|
Now, field resolution is defined with an instance method:
1 2 3 4 5 6 7 |
|
How does this address the issues listed above?
First, using classes reduces the “WTF” factor of GraphQL definition code. A seasoned Ruby developer might (rightly) smell foul play and reject GraphQL-Ruby on principle. (I was not seasoned enough to detect this when I designed the API!)
Proc literals are rare in Ruby, but common in GraphQL-Ruby’s .define { ... }
API. Their lexical scoping rules are different than method scoping rules, making it hard to remember what was and wasn’t in scope during field resolution (for example, what was self
?). To make matters worse, some of the blocks in the .define
API were instance_eval
’d, so their self
would be overridden. Practically, this meant that typos in development resulted in strange NoMethodError
s.
Proc literals also have performance downsides: they’re not optimized by CRuby, so they’re slower than method calls. Since they capture a lexical scope, they may also have unexpected impacts on memory footprint (any local variable may be retained, since it might be accessed by the proc). The solutions here are simple: just use methods, the way Ruby wants you to! 😬
In the new class-based API, there are no proc literals (although they’re supported for compatibility’s sake). There are some instance_eval
’d blocks (field(...) { }
, for example), but field resolution is just an instance method and the type definition is a normal class, so module scoping works normally. (Contrast that with the constant assignment in Types::Post = GraphQL::ObjectType.define { ... }
, where no module scope is used). Several hooks that were previously specified as procs are now class methods, such as resolve_type
and coerce_input
(for scalars).
Overriding !
is another particular no-no I’m correcting. At the time, I thought, “what a cool way to bring a GraphQL concept into Ruby!” This is because GraphQL non-null types are expressed with !
:
1 2 |
|
So, why not express the concept with Ruby’s !
method (which is usually used for negation)?
1
|
|
As it turns out, there are several good reasons for why not!
!
breaks the negation operator. ActiveSupport’s .present?
didn’t work with type objects, because !
didn’t return false
, it returned a non-null type.!
operator throws people off. When a newcomer sees GraphQL-Ruby sample code, they have a WTF moment, followed by the dreadful memory (or discovery) that Ruby allows you to override !
.So, overriding !
didn’t deliver any value, but it did present a roadblock to developers and break some really essential code.
In the new API, nullability is expressed with the options null:
and required:
instead of with !
. (But, you can re-activate that override for compatibility while you transition to the new API.)
By switching to Ruby’s happy path of classes and methods, we can help Ruby developers feel more at home in GraphQL definitions. Additionally, we avoid some unfamiliar gotchas of procs and clear a path for removing the !
override.
Rails’ automatic constant loading is wonderful … until it’s not! GraphQL-Ruby didn’t play well with Rails’ constant loading especially when it came to cyclical dependencies, and here’s why.
Imagine a typical .define
-style type definition, like this:
1
|
|
We’re assigning the constant Types::T
to the return value of .define { ... }
. Consequently, the constant is not defined until .define
returns.
Let’s expand the example to two type definitions:
1 2 |
|
If T1
depends on T2
, and T2
depends on T1
, how can this work? (For example, imagine a Post
type whose author
field returns a User
, and a User
type whose posts
field returns a list of Post
s. This kind of cyclical dependency is common!) GraphQL-Ruby’s solution was to adopt a JavaScriptism, a thunk. (Technically, I guess it’s a functional programming-ism, but I got it from graphql-js
.) A thunk is an anonymous function used to defer the resolution of a value. For example, if we have code like this:
1 2 |
|
GraphQL-Ruby would accept this:
1 2 |
|
Later, GraphQL-Ruby would .call
the proc and get the value. At that type, Types::User
would properly resolve to the correct type. This worked but it had two big downsides:
Proc
) in an unfamiliar context (a method argument), so it was frustrating and disorienting.How does switching to classes resolve this issue? To ask the same question, how come we don’t experience this problem with normal Rails models?
Part of the answer has to do with how classes are evaluated. Consider two classes in two different files:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Notice that Post
depends on User
, and User
depends on Post
. The difference is how these lines are evaluated, and when the constants become defined. Here’s the same code, with numbering to indicate the order that lines are evaluated:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Since Types::Post
is initialized first, then built-up by the following lines of code, it’s available to Types::User
in the case of a circular dependency. As a result, the thunk is not necessary.
This approach isn’t a silver bullet – Types::Post
is not fully initialized by the time Types::User
needs it – but it reduces visual friction and generally plays nice with Rails out of the box.
I’ve used a naughty word here, but in fact, I’m talking about something very good. Have you ever been stuck with some dependency that didn’t quite fit your application? (Or, maybe you were stuck on an old version, or your app needed a new feature that wasn’t quite supported by the library.) Like it or not, sometimes the only way forward in a case like that is to hack it: reopen classes, redefine methods, mess with the inheritance chain, etc. Yes, those choices come with maintenance downsides, but sometimes they’re really the best way forward.
On the other hand, really flexible libraries are ready for you to come and extend them. For example, they might provide base classes for you to extend, with the assumption that you’ll override and implement certain methods. In that case, the same hacking techniques listed above have found their time to shine.
ActiveRecord::Base
is a great example of both cases: plenty of libraries hack methods right into the built-in class (for example, acts_as_{whatever}
), and also, lots of Rails apps use an ApplicationRecord
class for their application-specific customizations.
Since GraphQL-Ruby didn’t use the familiar arrangement of classes and methods, it was closed to this kind of extension. (Ok, you could do it, but it was a lot of work! And who wants to do that!?) In place of this, GraphQL-Ruby had yet-another-API for extending its DSL. Yet another thing to learn, with more Proc literals 😪.
Using classes simplifies this process because you can use familiar Ruby techniques to build your GraphQL schema. For example, if you want to share code between field resolvers, you can include
a module and call its methods. If you want to make shorthands for common cases in your app, you can use your Base
type classes. If you want to add special configuration to your types, you can use class methods. And, whenever that day should come, when you need to monkey-patch GraphQL-Ruby internals, I hope you’ll be able to find the right spot to do it!
GraphQL-Ruby is three years old now, and I’ve learned a LOT during that time! I’m really thankful for the opportunity to focus on developer productivity in the last few months, learning how I’ve prevented it and working on ways to improve it. I hope to keep working on topics like this – how to make GraphQL more productive for Ruby developers – in the next year, especially, so if you have feedback on this new API, please open an issue to share it!
I’m excited to see how this new API changes the way people think about GraphQL in Ruby, and I hope it will foster more creativity and stability.
]]>Part of Ruby’s appeal is to be free of the cruft of its predecessors. So why is there so much interest in adding types to Ruby?
What are the benefits?
To experience a great type system in a Ruby-like language, I recommend Crystal.
Jeff Foster is a professor at the University of Maryland, College Park and works in the programming languages group. Along with his students, he’s been exploring Ruby type checkers for nine years! This year, he gave a presentation at StrangeLoop, Type Checking Ruby.
He described his various avenues of research over the years, and how they influenced one another, leading to a final question:
1 2 3 4 5 6 7 8 9 |
|
His early work revolved around static type checking: annotations in the source code were given to a type checker, which used those annotations to assert that the Ruby code was correct.
This approach had a fundamental limitation: how can dynamically-created methods (like Talk#owner
above) be statically annotated?
This drove him and his team to develop RDL, a dynamic type checker. In RDL, types are declared using methods instead of annotations, for example:
1 2 3 4 |
|
By using methods, it handles metaprogramming in a straightforward way. It hooks into Rails’ .belongs_to
and adds annotations for the generated methods, for example:
1 2 3 4 5 6 7 8 9 |
|
(In reality, RDL uses conditions, not monkey-patching, to achieve this.)
In this approach, type information is gathered while the program runs, but the typecheck is deferred until the method is called. At that point, RDL checks the source code (static information) using the runtime data (dynamic information). For this reason, RDL is called “Just-in-Time Static Type Checking.”
You can learn more about RDL in several places:
Personally, I can’t wait to take RDL for a try. At the conference, Jeff mentioned that type inference was on his radar. That would take RDL to the next level!
Not to read into it too far, but it looks like Stripe is exploring RDL 😎.
Soutaro Matsumoto also has significant academic experience with type checking Ruby, and this year, he presented some of his work at RubyKaigi in Type Checking Ruby Programs with Annotations.
He begins with an overview of type checking Ruby, and surveys the previous work in type inference. He also points out how requirements should be relaxed for Ruby:
Then, he introduces his recent project, Steep.
Steep’s approach is familiar, but new to Ruby. It has three steps:
.rbi
file which describes the types in your program, using a special type language, for example:1 2 3 |
|
1 2 3 4 |
|
Some connections between Ruby source and the .rbi
files can be made automatically; others require explicit annotations.
Run the type checker:
$ steep check app/models/talk.rb
It reminds me a bit of the .h
/.c
files in a C project.
Soutaro is also presenting his work at this winter’s RubyConf.
Valentin works at JetBrains (creators of RubyMine) and presented his work on type-checking based on runtime data. His presentation, Automated Type Contracts Generation for Ruby, was really fascinating and offered a promising glimpse of what a Ruby type ecosystem could be.
Valentin started by covering RubyMine’s current type checking system:
obj.execute
, what method does it call?He also pointed out that even code coverage is not enough: 100% code coverage does not guarantee that all possible codepaths were run. For example, any composition of if
branches require a cross-product of codepaths, not only that each line is executed once. Besides that, code coverage does not analyze the coverage of your dependencies’ code (ie, RubyGems).
So, Valentin suggests getting more from our unit tests: what if we observed the running program, and kept notes about what values were passed around and how they were used? In this arrangement, that runtime data could be accumulated, then used for type checking.
Impressively, he introduced the implementation of this, first using a TracePoint, then digging into the Ruby VM to get even more granular data.
However, the gathered data can be very complicated. For example, how can we understand the input type of String#split
?
1 2 3 4 5 6 7 8 9 10 11 |
|
Valentin showed how a classic technique, finite automata, can be used to reduce this information to a useful data structure.
Then, this runtime data can be used to generate type annotations (as YARD docs).
Finally, he imagines a type ecosystem for Ruby:
Personally, I think this is a great future to pursue:
You can see the project on GitHub: https://github.com/JetBrains/ruby-type-inference
There’s a lot of technically-savvy and academically-informed work on type checking Ruby! Many of the techniques preserve Ruby’s productivity and dynamism while improving the developer experience and confidence. What makes them unique is their use of runtime data, to observe the program in action, then make assertions about the source code.
]]>react-rails
2.0 🎊.
Here are a few highlights. For the full list, see the changelog!
Webpacker was great to work with. react-rails
now supports webpacker for:
<%= react_component(...) %>
via require
server_rendering.js
)A nice advantage of using webpacker is that you can load React.js from NPM instead of the react-rails
gem. This way, you aren’t bound to the React.js version which is included with the Ruby gem. You can pick any version you want!
To support frontends built with Node.js, react-rails
’s UJS driver is available on NPM as react_ujs
. It performs setup during require
, so these two are equal:
1 2 3 4 5 |
|
If you’re prerendering your React components on the server, you can perform setup and teardown in your Rails controller. For example, you might use these hooks to populate a flux store.
First, add the per_request_react_rails_prerenderer
helper to your controller:
1 2 3 4 |
|
Then, you can access react_rails_prerenderer
in the controller action:
1 2 3 4 5 6 |
|
That way, you can properly prepare & clean up a JS VM for server rendering.
Previously, ReactRailsUJS
“automatically” detected which libraries you were using and hooked up to their events for rendering components.
It still checks for libraries during its initial load, but you can also re-check as needed:
1 2 |
|
This function removes previous event handlers, so it’s safe to call anytime. (This was added in 2.0.2
.)
See the changelog for bug fixes and a new default server rendering configuration.
Webpacker is great! Setup was smooth and the APIs were clear and convenient. I’m looking forward to using it more.
🍻 Here’s to another major version of react-rails
!
Rails knows to watch config/routes.rb
for changes and reload them when the files change. You can use the same mechanism to watch other files and take action when they change.
I used this feature for react-rails server rendering and for GraphQL::Pro static queries.
Every Rails app has a @reloader
, which is a local subclass of ActiveSupport::Reloader
. It’s used whenever you call reload!
in the Rails console.
It’s attached to a rack middleware which calls #run!
(which, in turn, calls the reload blocks if it detects changes).
You can add custom preparation hooks with config.to_prepare
:
1 2 3 4 5 |
|
When Rails detects a change, this block will be called. It’s implemented by registering the block with app.reloader
.
To add new conditions for which Rails should reload, you can add to the app.reloaders
array:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
The object’s updated?
method will be called by the reloader. If any reloader returns true
, the middleware will run all to_prepare
blocks (via the call to @reloader.run!
).
Rails includes a goodie for watching files. ActiveSupport::FileUpdateChecker
is great for:
config/routes.rb
is watched this way)app/**/*.rb
is watched this way)You can create your own FileUpdateChecker
and add it to app.reloaders
to reload Rails when certain files change:
1 2 3 4 |
|
Some filesystems support an evented file watcher implementation, ActiveSupport::EventedFileUpdateChecker
. app.config.file_watcher
will return the proper filewatcher class for the current context.
1
|
|
react-rails
maintains a pool of V8 instances for server rendering React components. These instances are initialized with a bunch of JavaScript code, and whenever a developer changes a JavaScript file, we need to reload them with the new code. This requires two steps:
app.reloaders
to detect changes to JavaScript filesto_prepare
hook to reload the JS instancesIt looks basically like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
The full implementation supports some customization. You can see similar (and more complicated) examples with routes reloading, i18n reloading and .rb
reloading.
Happy reloading!
]]>In fact, loading a schema this way has been supported for while, but 1.5.0 adds the ability to specify field resolution behavior.
Besides queries, GraphQL has an interface definition language (IDL) for expressing a schema’s structure. For example:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
You can turn a definition into a schema with Schema.from_definition
:
1 2 |
|
(By the way, the IDL is technically in RFC stage.)
Schema.from_definition
also accepts default_resolve:
argument. It expects one of two inputs:
Hash<String => Hash<String => #call(obj, args, ctx)>>
; or#call(type, field, obj, args, ctx)
When you’re using a hash:
#call(obj, args, ctx)
)To get started, you can write the hash manually:
1 2 3 4 5 6 7 8 9 10 |
|
But you can also reduce a lot of boilerplate by using a hash with default values:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Isn’t that a nice way to set up a simple schema?
You can provide a single callable that responds to #call(type, field, obj, args, ctx)
. What a mouthful!
The advantage of that hefty method signature is that it’s enough to specify any resolution behavior you can imagine. For example, you could create a system where type modules were found by name, then methods were called by name:
1 2 3 4 5 6 7 8 9 10 11 |
|
So, a single function combined with Ruby’s flexibility and power opens a lot of doors!
Doesn’t it remind you a bit of method dispatch? The arguments are:
GraphQL Field Resolution | Method Dispatch |
---|---|
type |
class |
field |
method |
obj |
receiver |
args |
method arguments |
ctx |
runtime state (cf mrb_state , RedisModuleCtx , or ErlNifEnv ) |
Some schemas need other configurations in order to run:
resolve_type
to support union and interface typesTo add these to a schema, use .redefine
:
1 2 3 4 5 |
|
Rails has proven that “Convention over Configuration” can be a very productive way to start new projects, so I’m interested in exploring convention-based APIs on top of this feature.
In the future, I’d like to add support for schema annotations in the form of directives, for example:
1 2 3 |
|
These could be used to customize resolution behavior. Cool!
]]>When modifying shared code or reconfiguring, it can be hard to tell how the schema will really change. To help with this, set up a snapshot test for your GraphQL schema! This way:
You can even track the schema from different contexts if you’re using GraphQL::Pro
’s authorization framework.
This approach was first described in GraphQL at Shopify.
Write a Rake task to get your schema’s definition and write it to a file:
1 2 3 4 5 6 7 8 9 10 |
|
You can run it from terminal:
1 2 |
|
This updates the file in your repo. Go ahead and check it in!
1 2 |
|
Any changes to the Ruby schema code must be reflected in the .graphql
file. You can give yourself a reminder by adding a test case which asserts that the GraphQL definition is up-to-date:
1 2 3 4 5 6 7 8 9 10 |
|
If the definition is stale, you’ll get a failed test:
This reminder is helpful in development and essential during code review!
Now that your schema definition is versioned along with your code, you can see changes during code review:
If your schema looks different to different users, you can track multiple schema dumps. This is helpful if:
:view
configuration of GraphQL::Pro
’s authorizationonly:
/ except:
to manually filter your schemaJust provide the context:
argument to Schema.to_definition
as if you were running a query. (Also provide only:
/except:
if you use them.)
Print with a filter from the Rake task:
1 2 3 4 5 6 7 8 |
|
Test with a filter from the test case:
1 2 3 4 5 6 7 |
|
Now you can keep an eye on the schema from several perspectives!
]]>graphql-ruby
1.5.0 will be released. Query execution will be ~70% faster than 1.3.0!
Let’s look at how we reduced the execution time between those two versions. Thanks to @theorygeek who optimized the middleware chain helped me pinpoint several other bottlenecks!
To track GraphQL execution overhead, I execute the introspection query on a fixture schema in graphql-ruby’s test suite.
On GraphQL 1.3.0, the benchmark ran around 22.5 iterations per second:
On master, it runs around 38 iterations per second:
That’s almost 1.7x faster!
1 2 |
|
So, how’d we do it?
To find where time was spent, I turned to ruby-prof. I wrapped GraphQL execution with profiling and inspected the result:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
A few things stood out:
Class#new
: this is time spent initializing new objects. I think initialization can also trigger garbage collection (if there’s not a spot on the free list), so this may include GC time.InstanceDefinable#ensure_defined
, which is part of graphql-ruby’s definition API. It’s all overhead to support the definition API, 😿.1748
times. Turns out, this is once per field in the response.25,403
seems like a lot of calls to Module#===
!Since Class#new
was the call with the most self
time, I thought I’d start there. What kind of objects are being allocated? We can filter the profile output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Lots of GraphQL internals! That’s good news though: those are within scope for optimization.
MiddlewareChain
was ripe for a refactor. In the old implementation, each field resolution created a middleware chain, then used it and discarded it. However, this was a waste of objects. Middlewares don’t change during query execution, so we should be able to reuse the same list of middlewares for each field.
This required a bit of refactoring, since the old implementation modified the array (with shift
) as it worked through middlewares. In the end, this improvement was added in 5549e0cf
. As a bonus, the number of created Array
s (shown by Array#initialize_copy
) also declined tremendously since they were used for MiddlewareChain
’s internal state. Also, calls to Array#shift
were removed, since the array was no longer modified:
1 2 3 4 |
|
🎉 !
The number FieldResult
objects was also reduced. FieldResult
is used for execution bookkeeping in some edge cases, but is often unneeded. So, we could optimize by removing the FieldResult
object when we had a plain value (and therefore no bookkeeping was needed): 07cbfa89
A very modest optimization was also applied to GraphQL::Arguments
, reusing the same object for empty argument lists (4b07c9b4
) and reusing the argument default values on a field-level basis (4956149d
).
Some elements of a GraphQL schema don’t change during execution. As long as this holds true, we can cache the results of some calculations and avoid recalculating them.
A simple caching approach is to use a hash whose keys are the inputs and whose values are the cached outputs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
The first lookup printed a message and returned a value but the second lookup did not print a value. This is because the block wasn’t called. Instead, the cached value was returned immediately.
This approach was applied aggressively to GraphQL::Schema::Warden
, an object which manages schema visibility on a query-by-query basis. Since the visibility of a schema member would remain constant during the query, we could cache the results of visibility checks: first 1a28b104
, then 27b36e89
.
This was also applied to field lookup in 133ed1b1e
and to lazy_resolve
handler lookup in 283fc19d
.
yield
Instead of &block
Due to the implementation of Ruby’s VM, calling a block with yield
is much faster than block.call
. @theorygeek
migrated MiddlewareChain
to use that approach instead in 517cec34
.
In order to handle circular definitions, graphql-ruby’s .define { ... }
blocks aren’t executed immediately. Instead, they’re stored and evaluated only when a definition-dependent value is required. To achieve this, all definition-dependent methods were preceeded by a call to ensure_defined
.
Maybe you remember that method from the very top of the profiler output above:
1 2 3 |
|
A fact about GraphQL::Schema
is that, by the time it is defined, all lazy definitions have been executed. This means that during query execution, calling ensure_defined
is always a waste!
I found a way to remove the overhead, but it was a huge hack. It works like this:
When a definition is added (with .define
):
find each definition-dependent method definition on the defined object and gather them into an array:
@pending_methods = method_names.map { |n| self.class.instance_method(n) }
ensure_defined
@pending_methods
, overriding the dummy methodsThis way, subsequent calls to definition-dependent methods don’t call ensure_defined
. ensure_defined
removed itself from the class definition after its work was done!
You can see the whole hack in 18d73a58
. For all my teasing, this is something that makes Ruby so powerful: if you can imagine it, you can code it!
Two minor releases later, the profile output is looking better! Here’s the output on master:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
Here are the wins:
ensure_defined
reduced by 100% 😆And, as shown in the benchmark above, 1.7x faster query execution!
There’s one caveat: these optimization apply to the GraphQL runtime only. Real GraphQL performance depends on more than that. It includes application-specific details like database access, remote API calls and application code performance.
]]>graphql
gem.
I haven’t tried this extensively, but I had to satisfy my curiosity!
Let’s say we have a GraphQL schema which has long-running IO- or system-bound tasks. Here’s a silly example where the long-running task is sleep
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Let’s consider a query like this one:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
How long will it take?
1 2 3 4 5 6 7 |
|
About 9 seconds: three sleep(3)
calls in a row.
The concurrent-ruby
gem includes Concurrent::Future
, which runs a block in another thread:
1 2 3 4 5 6 7 8 |
|
We can use it to put our sleep(3)
calls in different threads. There are two steps.
First, use a Concurrent::Future
in the resolve function:
1 2 3 4 5 6 |
|
Then, tell the Schema to handle Concurrent::Future
s by calling #value
on them:
1 2 3 4 |
|
Finally, run the same query again:
1 2 3 4 5 6 7 |
|
🎉 Three seconds! Since the sleep(3)
calls were in different threads, they were executed in parallel.
Ruby can run IO operations in parallel. This includes filesystem operations and socket reads (eg, HTTP requests and database operations).
So, you could make external requests inside a Concurrent::Future
, for example:
1 2 3 |
|
Or, make a long-running database call inside a Concurrent::Future
:
1 2 3 |
|
Switching threads incurs some overhead, so multithreading won’t be worth it for very fast IO operations.
GraphQL doesn’t know which resolvers will finish first. Instead, it starts each one, then blocks until the first one is finished. This means that subsequent long-running fields may have to wait longer than they “really” need to. For example, consider this query:
1 2 3 4 5 6 |
|
Even with multithreading, this would take about 7 seconds to execute. First, GraphQL would wait for sleep(for: 5)
, then it would get to nestedSleep(for: 2)
, which would have already finished, then it would execute sleep(for: 2)
.
If your GraphQL schema is wrapping pre-existing HTTP APIs, using a technique like this could reduce your GraphQL response time.
]]>graphql-ruby
is almost two years old! Today, I’m adding a new element to the project, GraphQL::Pro
.
I have three goals with GraphQL::Pro
:
Additionally, I’m starting a GraphQL Ruby newsletter.
Today, GraphQL::Pro
provides some integrations with third-party tools:
As time goes on, I’ll keep an eye out for other integrations that could be included in GraphQL::Pro
. (If you have a suggestion, I’d love to hear it!)
Some teams adopt GraphQL as a foundational element of their application. I’d like to provide them service (and peace of mind) as they build on that investment. GraphQL::Pro
customers have my ear for any performance issues, bugs or feature requests. They also have an assurance that I’ll continue to maintain and improve graphql-ruby
.
I really enjoy working on graphql-ruby
and I’m excited about the work to be done in 2017. But it’s no secret that open-source work can become an unrewarding, thankless grind. Charging money for GraphQL::Pro
provides me with a simple, concrete “reward” to continue the work. I hope this will be good for me, for the project, and for others who are invested in the project.
If any of this sounds good to you, you can buy GraphQL::Pro
at http://graphql.pro !
raise
is return
’s evil twin.
They both stop the execution of the current method. After a return
, nothing else is executed. After a raise
, nothing else is executed … maybe. The method may have a rescue
or ensure
clause which is executed after the raise
, so a reader must check for those.
They both change flow of control. return
gives control back to the caller. raise
may give control anywhere on the call stack, depending on the specific error and rescue
clauses. If all you see is a raise
, you can’t guess where it will be rescued!
They both send values to their new destination. return
provides the given value to the caller, who may capture the return value in a local variable. raise
provides the error object to the rescue
-er. return
can send any kind of value, but raise
can only send error objects.
They both create coupling across call stack frames. return
couples two adjacent call stack frames: caller depends on the return value. raise
→ rescue
couples far-removed stack frames: they may be adjacent, or they may be several frames removed from one another.
Sending values through a program by calling methods and return
-ing values is very predictable. If you return a different value, the caller will get a different value. To see where return values “go”, simply search for calls to that method.
Finding where raise
’d errors go is a bit more challenging. For example, this change:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
How can you tell if this is a safe refactor? Here are some considerations:
rescue
s, you have to keep the error’s ancestry in mind, finding bare rescue
s, superclass-tagged rescue
s and class-tagged rescue
s.rescue
s may consume the error object itself. For example, they may read its #message
or other attached data. If you change any properties of the error object, you may break the assumptions of those rescue
s.rescue
’d differently, you must also consider how execution flow will change in other methods. For example, some methods may be cut short because previously-rescue
’d errors now propagate through them. Other methods which used to be cut short may now continue running, since errors are rescued in child method calls.If your raise
is located in a Ruby gem, these problems are even harder, because rescue
clauses may exist in your users’ code.
If your error patterns are well documented, ༼ つ ◕_◕ ༽つ 🏆
. Bravo, just don’t break your public API. Users might still make assumptions beyond the documentation, such as error ancestry or message values. Additionally, they could be monkey-patching library methods and applying rescue
-related assumptions to those patches.
If your error patterns aren’t documented, 💩 ノ༼ ◕_◕ ノ ༽
. You have no idea what assumptions users make about those errors! You can’t be sure your changes won’t break their code.
raise
can be replaced by return
. However, if you’re using raise
to traverse many levels of the call stack, the refactor will be intense. Take heart: previously you were hacking your way back up the call stack, now you’re creating a predictable, explicit flow through your program!
It’s worth repeating, don’t use exceptions for flow control.
Here are some techniques for expressing failures with return
.
1 2 3 4 5 6 7 8 9 10 11 |
|
StandardError
instance to the caller, use a Failure
class to communicate failure. Additionally, use a Success
class to communicate success. (This is similar to the “monad” technique, eg dry-monads
gem.)1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
As a last resort, return nil
. Using nil
as an expression of failure has some downsides:
nil
can’t hold a message or any extra datanil
is a valid valueBut, for simple operations, using nil
may be sufficient. Since it will be communicated via return
, refactoring it will be straightforward in the future!
raise
has its purposes.
raise
is a great way to signal that the program has reached a completely unexpected state and that it should exit. For example, in the convert_file
example above, we could use raise
to assert that we don’t receive an unexpected value from convert_file
:
1 2 3 4 5 6 7 8 9 |
|
Now, if the method ever returns some unexpected value, we’ll receive a loud failure. Some people use fail
in this case, which is also fine. However, the need to disambiguate raise
and fail
is a code smell: stop using raise
for non-emergencies!
raise
is also helpful for re-raising other errors. For example, if your library needs to log something when an error happens, it might need to capture the error, then re-raise it. For example:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
This way, you can respond to the error without disrupting user code.
In my own work, I’m transitioning away from raising errors and towards communicating failure by return values. This pattern is ubiquitous in languages like Go and Elixir. In Node.js, callbacks communicate errors in a similar way (callback arguments). I think Ruby code can benefit from this practice as well.
]]>