* Catch name collisions within type
If two names of the same type (for example, two structs) exist in the same
file, one of them will be discarded silently at parse time. This causes it to
raise an exception.
* Clean up exception message
My previous attempt to consolidate these two mix tasks into one was
unsuccessful due to the way mix tasks receive their command line
arguments. Specifically, we don't have a good way to differentiate
between e.g. a `mix test` command line containing test script filenames
and a `mix compile.thrift` command line naming Thrift schema files. When
the `:thrift` compiler is included, it will also run in the `mix test`
task flow and get confused about its inputs.
We now have two distinct and purpose-built mix tasks:
- `compile.thrift` - intended for use in the Mix.compilers list
- `thrift.generate` - intended for interactive command-line use
Previously the resolver was a separate process that just maintained a map. This
had the drawback that it was difficult to catch errors raised during the
resolution process. We'd to be able to raise such an error on name collisions.
We previously defined nearly identical sets of constants representing
the binary protocol's field types as attributes in each module that
required them. This duplication isn't great for long-term maintenance.
This approach defines a single set of type constants as macros in a
new Thrift.Protocol.Binary.Type module. Using macros lets us preserve
all of the compile-time benefits we were getting from the previous
approach while still supporting code sharing.
* Timeout / Retry refactor
Servers now support read timeouts
Additionally, I changed how we handle retries due to some idiosyncracies
with how erlang responds to server failures.
When the server closes the connection, the client's `:gen_tcp.send`
operation will always succeed, failing instead on the corresponding
`:gen_tcp.recv` command. This is problematic, because we can't know for
sure that a message has been sent, and if we retry, we might resend a
message accidentally.
Furthermore, we can _never_ really know if a oneway message has been
sent or not.
Admittedly, the window for this is pretty narrow, the server would have
to sever the connection between the send and recv calls. The next case
is more troublesome.
Since we have backoff behavior, imagine if the user has set a retry
count of `:infinity` and made a call to a dead server. Now the client is
backing off and the `GenServer` call will timeout after 5 seconds. The
client is now stuck in a loop, reconnecting forever.
In light of these issues, I've removed repeated reconnects from the
client and have implemented a one-shot reconnect. We can still send a
message twice, but the window is sufficiently small to make me not worry
so much. This also has the effect of improving UX in the light of a
server that disconnects clients after a short timeout. If you send a
message on a disconnected client, it remembers it, reconnects and sends
it.
That seems reasonable.
Generated module names can be quite long, so this pattern produces very long
lines because Macro.to_string does not wrap pipe operators to the next line.
serialized_var = %Big.Generated.Module{} |> Big.Generated.Module.serialize()
This change just replaces it with a slightly more readable:
var = %Big.Generated.Module{}
Big.GeneratedModule.serialize(var)
* Refactor Client generated/static interface
This moves message serialization completely into Binary.Framed.Client, and
response decoding completely out into the generated client. Their
responsibilities are more coherent. It also puts us into position for future
potential improvements, such as moving seq_id management into the connection
process's state, and generating code to match response exceptions in the way we
currently match unions.
* Return TApplicationException for invalid message type
* Keep seq_id in connection process state
* PR feedback
The tmp directory is used to store temporary .thrift and generated files by
ThriftTestCase. It's typically around during development and contains files but
they should not be checked in.
Seems to have been overlooked. We implemented strings, which are effectively
the same in Elixir, but didn't check specifically for a type of :binary in
every situation.
Previously when the parser found a reference where it expected a static value,
it treated it as an enum. However it could also be a const. Now the parser
turns any reference into a StructRef and leaves it to the next layer to resolve
the reference.
Fixes#140.
* Server improvements
The server's start_link didn't actually start_link. I've changed this to
bring the server's supervision into the user's application.
Also, we weren't properly cleaning up after server errors, so now I just crash.
* Allow 'default required' fields to be nil
We discovered that other Thrift implementations we use internally do not
serialize unset 'default required' fields. This change brings our
implementation's behavior in line with the others.
* Test that optional fields are serialized
- Our command line options now more closely mimic the Apache Thrift
compiler's. We support both long and "short" option formats.
- The compile task's output now simply prints "Compiling N files", which
is the Elixir 1.3+ output convention. Use the `--verbose` command line
option to see per-file output.
- The compiler task's unit test helper code has been simplified a bit.
Enforce field requiredness during serialization
The IDL spec [1] states that required fields must always be written during
serialization. There are also "default required" fields, i.e. fields specified
neither as required or optional. Apparently they also should always be written,
though there are exceptions to which the spec alludes but about which it does
not deign to elaborate.
[1]: https://thrift.apache.org/docs/idl#field-requiredness
This gives us the ability to use 1.3+ APIs as we develop our 2.0 branch.
Many other libraries (Absinthe, Dialyxir) also have a 1.3+ requirement.
Also, Elixir 1.4 is imminent, so add that to our testing matrix too.