Compare commits

...
Author SHA1 Message Date
José Valim b1a7afd061 Release v1.19.1 2025-10-20 17:30:38 +02:00
Travis Vander Hoop e9bf5df25a Update documentation for formatter's :excludes option (#14842) 2025-10-18 20:35:08 +02:00
José Valim 2e7bb9b47f Do not spawn partitions when all dependencies are local and ok, closes #14843 2025-10-18 19:07:28 +02:00
José Valim 83c68cecc1 Simplify computation of mismatched parts for error messages
Before we were doing:

    common = intersection(actual, expected)
    uncommon = difference(actual, common)

But the second clause is:

    actual and not (actual and expected)

Which is literally the same as:

    actual and not expected

But much faster as it avoids the large nesting of BDDs.

Closes #14836.
2025-10-18 18:54:39 +02:00
José Valim 184b724483 More optimizations for differences
* when a1 < a2
* when a1 == a2 and c2 == bottom and d2 == bottom
* when a1 == a2 and u2 == bottom
2025-10-18 18:33:18 +02:00
José Valim 47fea9580a Perform expensive operation once 2025-10-18 18:32:44 +02:00
José Valim 6a6678f337 Do not attempt to touch deleted files 2025-10-18 18:32:32 +02:00
José Valim ec87c6b111 Improve protocol violation warnings 2025-10-18 18:32:28 +02:00
José Valim 0c595489b6 Improve protocol type error to list possible root causes 2025-10-18 18:32:22 +02:00
Eric Meadows-Jönsson cdbfa09edb Fix hex upload of Elixir build without -otp- suffix (#14840)
OTP 25 is no longer supported so we didn't upload a generic build.
Instead find the oldest version instead of hardcoding it.
2025-10-17 22:21:14 +02:00
José Valim baa25991fa Do not escape dbg options, closes #14839 2025-10-17 22:15:22 +02:00
Daniil Kulchenko a5c9121a7c Fix EEx.compile_string passing invalid options to tokenize (#14835)
EEx.compile_string/2 was passing all options to tokenize/2, but
tokenize/2 only accepts tokenize_opt (:file, :line, :column,
:indentation, :trim). This caused dialyzer to correctly flag calls
with :engine or :parser_options as type errors in Elixir 1.19+.

The fix filters options before passing to tokenize/2, keeping only
the valid tokenize_opt keys, while still passing the full options
list to EEx.Compiler.compile/3 which needs :engine, :parser_options,
and custom engine options.

Fixes #14834
2025-10-16 23:49:53 +02:00
José Valim 177dc8ab59 Release v1.19.0 2025-10-16 08:52:04 +02:00
José Valim bba8835ecc Fix IEx parser fetching on mix release 2025-10-16 08:46:48 +02:00
José Valim dea8456022 Escape meta within existing quote extensions (#14832)
Closes #14829
Closes #14830
2025-10-14 13:05:44 +02:00
José Valim 544a10ccfa Update CHANGELOG 2025-10-09 10:08:12 +02:00
José Valim ee14357bcd Add --shell to mix cmd (#14827) 2025-10-09 10:07:18 +02:00
José Valim 2fa136e60a Deal with relative paths in mix cmd, closes #14787 (#14788) 2025-10-09 09:27:16 +02:00
Eksperimental f382affce3 Fix typos in v1.19 (#14826) 2025-10-09 08:48:14 +02:00
José Valim 36190985d2 Include a hint for defimpl type checking 2025-10-08 18:27:39 +02:00
José Valim 957f84b7cd Release v1.19.0-rc.2 2025-10-07 18:10:32 +02:00
José Valim 74fab3caf4 Update parallel compiler docs, closes #14821 2025-10-07 17:54:41 +02:00
Eksperimental f1a6a4e019 Improve Kernel.ParallelCompiler warning (#14820) 2025-10-07 17:54:41 +02:00
Eksperimental c7884ca393 Elixir v1.19 introduces a warning related to structs, (#14818)
hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of:

         user = some_fun()
         %User{user | name: "John Doe"}

     it is enough to write:

         %User{} = user = some_fun()
         %{user | name: "John Doe"}

Since this could be seen by new-comers to the language, offering a better user experience by avoiding abbreviations. Favoring the usage of "some_function" instead of "some_fun"
2025-10-07 17:54:41 +02:00
José Valim 6f3fb272af Add mix help app:APP, closes #14782 2025-10-07 17:47:31 +02:00
José Valim 8c14d3a818 Add newline after inspection, closes #14819 2025-10-07 17:15:24 +02:00
José Valim 9e10ea876f Update CHANGELOG 2025-10-07 15:58:51 +02:00
José Valim 64f69cfee8 Improve error message when escaping default values with custom rules in structs, closes #14817 2025-10-07 14:26:11 +02:00
José Valim 7844c98fe7 Ensure escaping works within struct fields, closes #14817 2025-10-07 13:07:42 +02:00
José Valim 519eff5e29 Do not crash on empty test unit groups, closes #14754 2025-10-07 11:19:23 +02:00
Rafał Studnicki e29c40a7e9 Add key-based partitioning to duplicate registries (#14654) 2025-10-06 19:02:01 +02:00
José Valim 9fc3708980 Fix optimizations for closed map checking (#14813) 2025-10-06 17:25:10 +02:00
Jonatan Männchen c06b6a929e Update ORT Scanner (#14594) 2025-10-06 08:39:20 +02:00
Wojtek Mach cfa3bb7429 Fix preloading modules in mix test --slowest-modules=N (#14811) 2025-10-05 21:46:49 +02:00
Art Kay 195a4cf9f7 Optimize Access.filter to eliminate intermediate list creation (#14749) 2025-10-05 15:38:42 +02:00
José Valim 8daa2a6f67 Checkpoint before verification to avoid ignore modules warnings 2025-10-05 15:33:41 +02:00
José Valim d38f331db6 Use a map to track Mix compiler state 2025-10-05 15:29:54 +02:00
José Valim f642b30e42 Release v1.19.0-rc.1 2025-10-05 12:13:11 +02:00
José Valim 23f5ade25c Release v1.19.0-rc.1 2025-10-05 12:07:50 +02:00
José Valim b0c1c35933 Fix regression on direct raise of ExUnit.AssertionError 2025-10-04 21:40:42 +02:00
José Valim 734d76a51b Store bdd leaves in a unified and compact format (#14807) 2025-10-04 13:12:38 +02:00
José Valim b2bf886884 Improve docs for test patterns 2025-10-04 13:10:41 +02:00
Jean Klingler d703e784f4 Fix inaccurate hint for disabling :test_ignore_filters (#14808) 2025-10-04 13:10:36 +02:00
José Valim 44fd817935 Use lazy bdd for all types (#14806)
We use Lazy BDDS: ternary trees (instead of binary) where the additional node
encodes a lazy union, as in "COVARIANCE AND CONTRAVARIANCE:
A FRESH LOOK AT AN OLD ISSUE", with some additional optimisations
for intersections and differences to avoid materialising unions.
2025-10-04 13:10:31 +02:00
José Valim f8dcd941ff Fix :file.pwrite return type 2025-10-01 14:44:40 +02:00
José Valim ce403643cd Include previous clause line on default errors/warnings, closes #14804 2025-10-01 08:45:01 +02:00
José Valim 668c9a8f5d Revert "Do not allow protocols to define structs nor exceptions, closes #14158"
Projects rely on this feature, therefore we have to revert
to avoid breaking changes.

This reverts commit 01c82022f2.

Closes #14803.
2025-10-01 08:21:28 +02:00
Eksperimental 3175901d89 Align Regex dotall modifier to PCRE2 (#14792) 2025-09-30 16:48:03 +02:00
José Valim a2baac915a Address regressions on 'not in' operator
Closes #14783.
2025-09-30 16:47:11 +02:00
Jonatan Kłosko 5a5e3855aa Improve user detection in lock and pubsub implementation (#14801) 2025-09-30 16:46:31 +02:00
Jonatan Kłosko cd580f679c Fix File.rename/2 race condition in lock implementation on windows (#14800) 2025-09-30 16:46:31 +02:00
José Valim 33347f107e Update CHANGELOG 2025-09-29 09:36:03 +02:00
José Valim cb15a3dd4c Convert line break error into a warning for v1.19 2025-09-29 09:26:15 +02:00
José Valim a4f98b1177 Optimize DNFs to avoid negations when possible 2025-09-28 20:01:26 +02:00
Guillaume Duboc 5b2f9c9201 Rewrite maps, tuples, lists as BDDs and improve performance (#14693) 2025-09-28 19:05:30 +02:00
Zach Daniel b969aead71 Make errors when piping expressions in IEx safer (#14795) 2025-09-28 16:18:23 +02:00
Jean Klingler bfbd7658b3 Assert scope is not match/guard when using escaped regexes (#14780) 2025-09-27 23:02:05 +09:00
José Valim 1327c0c5c3 Improve docs for shift vs add 2025-09-22 10:42:54 +02:00
Jean Klingler 64c61df2bf Warn on boot for OTP28.0 (#14732) 2025-09-18 19:10:12 +09:00
Jean Klingler 4363dffaf5 Fix Macro.escape/1 bug when :quote tuples is in the tail of a list (#14775)
Close https://github.com/elixir-lang/elixir/issues/14771
2025-09-17 07:15:52 +09:00
Jean Klingler 7be008c1b0 Bugfix: Macro.escape/1 properly escapes meta in :quote tuples (#14773)
Close #14771

Also internally renames the `op` field inside `elixir_quote`: `none -> escape`, `prune_metadata` -> `escape_and_prune`, `add_context -> quote`.
2025-09-16 18:37:16 +09:00
Jonatan Männchen 499a9c55d3 Correct builds.hex.pm Publish Condition in CI (#14772) 2025-09-15 23:14:27 +02:00
José Valim b18cdb0f7a Do not persist temporary compilation warnings, closes #14768 2025-09-14 10:45:01 +02:00
José Valim 62949b0728 Revert "Convert verification errors into diagnostics, closes #14768"
This reverts commit 2dc3d2713c.

This solution will still leave a corrupted state if
verification is manually aborted.
2025-09-14 10:44:58 +02:00
José Valim cb108d5ce5 Unify mix recompile and external resource handling
Imagine the following scenario:

1. the user changes an external resource
2. the user calls mix compile and it fails
3. the user reverts the changes to the external resource

We had already fixed this bug for `__mix_recompile__?`.
Therefore, this pull request unifies how both are handled
by touching the source file after we detect it is stale
(which is also what we do on every subsequent compilation
cycle).

Note that before we stored the result of `__mix_recompile__?`
in the checkpoint file. However, that's not needed. The checkpoint
is useful to track changes to sources that may not define any module.
But since the compilation check and external resources always
require a module, touching the file is enough.

Closes https://github.com/phoenixframework/phoenix/issues/6476.
2025-09-13 21:32:04 +02:00
José Valim 1f7ddd7acd Convert verification errors into diagnostics, closes #14768 2025-09-13 20:07:44 +02:00
José Valim 7477c9326e Expand on why we supervise, not how (#14764)
Closes #14763.
2025-09-10 22:16:01 +02:00
Jean Klingler c0e334df2a Add missing :generated to Macro.escape_opts/0 type (#14761) 2025-09-10 20:14:26 +09:00
José Valim fd9dbf8490 Update Unicode to version 17.0.0 (#14760)
This is an automated commit created by the Maintenance project
https://github.com/eksperimental/maintenance

Please read the release notes by visiting
<http://www.unicode.org/versions/Unicode17.0.0/>.
2025-09-10 09:10:32 +02:00
Jean Klingler 7f45befed6 Have mix test fail if warnings and --warnings-as-errors (#14756) 2025-09-10 07:46:45 +09:00
Nathan Long bbcd7f3094 ExUnit sets a process label for each test (#14758) 2025-09-09 21:53:24 +02:00
José Valim 51ac9bc324 Accept any enumerable in Logger.metadata/1 2025-09-09 15:31:19 +02:00
Jean Klingler c8f8d02cc0 Fix dialyzer opaqueness warnings on module attrs in OTP28 (#14755)
* Mark module attributes as generated in case they contain opaque terms

* Add and use Macro.escape(ast, generated: true)
2025-09-09 17:51:50 +09:00
Jean Klingler d0634c9188 Fix infinite loop: Enum.take/2 with negative index on empty enum (#14747) 2025-09-05 21:25:51 +09:00
Jonatan Männchen cc797de249 tighten CI secret scope and move AWS config to environment vars (#14627)
* Add `environment: release` to the "publish-to-hex" job so that only
  workflows explicitly targeting the release environment can read
  sensitive values.
* Gate the job behind `if: ${{ vars.HEX_AWS_REGION }}` to avoid noisy
  failures in forks where the variable is not configured.
* Replace `${{ secrets.HEX_AWS_REGION }}` / `${{ secrets.HEX_AWS_S3_BUCKET }}`
  references with `${{ vars.* }}`.  These are not credentials, so
  environment-level *variables* are a better fit and keep them readable
  only by jobs that declare the environment.
* Remove Fastly secrets from the job-wide `env:` block and inject them
  only into the Fastly purge step, following the principle of least
  privilege.  Other steps no longer see these tokens.

Restricting secret visibility to an environment and to the exact step
that needs them reduces the blast radius of a compromised workflow run,
blocks accidental exposure in logs of unrelated steps, and stops forks
from obtaining privileged data.
2025-09-03 22:14:58 +02:00
José Valim f56139aa2c Add closing token metadata to a.{}, closes #14682 2025-08-31 22:10:01 +02:00
José Valim 8ac8230e18 Properly handle column for 'in' in 'not in' operator
Closes #14681.
2025-08-31 22:10:00 +02:00
sabiwara 0fca9beaf7 Remove test for warning disabled on 1.19 2025-08-31 20:10:59 +09:00
Jean Klingler b3e3e8c6aa Do not consider variables from pattern in bitstring modifier (#14738) 2025-08-31 19:21:15 +09:00
José Valim 61603a8725 Update CHANGELOG 2025-08-31 11:20:33 +02:00
José Valim 44c5069573 Update CHANGELOG 2025-08-31 11:11:53 +02:00
Jesse Stimpson a96c04f260 Improve docs on iex remote shell halt behaviour (#14721) 2025-08-31 10:59:33 +02:00
Eksperimental d458fcb9f5 Fix order in ExUnit results when listing pinned variables (#14723)
The pinned variables were returned in a random order (often reversed):

     test/ex_unit_pinned_variables_order_test.exs:23
     match (=) failed
     The following variables were pinned:
       var_d = "four"
       var_c = "three"
       var_b = "two"
       var_a = "one"
     code:  assert %{a: ^var_d, b: ^var_c, c: ^var_b, d: ^var_a} = build(var_a, var_b, var_c, var_d)
     left:  %{a: ^var_d, b: ^var_c, c: ^var_b, d: ^var_a}
     right: %{a: "one", b: "two", c: "three", d: "four"}
     stacktrace:
       test/ex_unit_pinned_variables_order_test.exs:29: (test)

This fix sorts them alphabetically.

This bug was introduced in 884e93391e when the pinned vars were now accumulated in a map (instead
of a list).

A repo replicating the issue can be found here:
- https://github.com/eksperimental-debug/elixir_debug/tree/ex-unit-pinned-variables-order
- https://github.com/eksperimental-debug/elixir_debug/blob/ex-unit-pinned-variables-order/ex_unit_pinned_variables_order/test/ex_unit_pinned_variables_order_test.exs
2025-08-31 10:58:58 +02:00
José Valim d507502ecd Update bidi/line break character checks according to UX#55 2025-08-31 10:57:43 +02:00
José Valim 2bb27ea128 Only break newlines if original char is a newline 2025-08-31 10:57:43 +02:00
Lukasz Samson 6fbc6e08a0 Advance line when processing ? followed by <LF> and \<LF>
Closes #14715.
2025-08-31 10:57:43 +02:00
José Valim 71bb017b61 Raise if message in AssertionError is not a binary
Closes #14695.
2025-08-31 10:57:43 +02:00
Łukasz Samson 1fadefe25f Catch-all clause for unbalanced terminators (#14694) 2025-08-31 10:57:43 +02:00
Jean Klingler e1f34d09af Shallow-validate the return of __escape__ (#14736) 2025-08-31 15:51:46 +09:00
Jean Klingler ff21a9d601 Add __escape__/1 and use it to fix Regex escaping in OTP28.1+ (#14720)
Leverages newly added :re.import/1.
https://github.com/erlang/otp/pull/9976
2025-08-30 18:29:56 +09:00
Jean Klingler 1778bf211c Inspect ill-formed structs as maps (#14718) 2025-08-23 17:16:20 +09:00
José Valim 99f2c8f2ea Fix docs for Macro.compile_apply/4 2025-08-17 09:53:59 +02:00
José Valim 54369ba3e5 Improve docs on DynamicSupervisor blocking operations 2025-08-15 19:39:20 +02:00
Steve Cohen ba0f3935d3 Fix filtering documentation (#14705)
The filtering documentation implied that the msg attribute of the
logger event map could be a binary, but according to the erlang types
(https://www.erlang.org/doc/apps/kernel/logger.html#t:log_event/0) it
can't be a binary.

This change updates the docs with an example that comports with the
actual typing.
2025-08-08 08:43:50 +02:00
Eksperimental b566edc2ab ExUnit: Raise explaining what failed on invalid tags (#14707) 2025-08-08 08:43:49 +02:00
Chris Hicks 297f1f3edf Add options to mix format to allow excluding of files (#14702) 2025-08-06 10:05:21 +02:00
Łukasz Samson 764235f1da Fix expand crash on invalid multialias root (#14698) 2025-08-06 10:05:20 +02:00
ice_cap 8d216b87cd Update structs.md (#14683) 2025-08-04 08:59:33 +02:00
José Valim 2c7fa47ad6 Remove general catch on sigil token 2025-07-28 08:28:58 +02:00
Łukasz Samson 71e1ddc64e Return error on invalid unicode sequences (#14666) 2025-07-27 19:04:52 +02:00
José Valim f50f35fd1c Add --name-pattern option to mix test and regex support to OptionParser (#14674) 2025-07-26 19:56:05 +02:00
José Valim 5bc81aaa0a Enhance OptionParser.ParseError with available options display (#14673)
Example output:

  Expected one of:
    --count INTEGER (alias: -c)
    --debug, --no-debug (alias: -d)
    --files STRING (alias: -f) (may be given more than once)
    --verbose, --no-verbose (alias: -v)

Prompt
======

When we raise ParseError, include all of the options we could
potentially accept, alongside their types and aliases. For example,
the switches `[foo: :string, bar: :integer]` and `aliases: [b: :bar]`,
the error message should say:

    Expected one of:
      --foo STRING
      --bar INTEGER (alias: -b)

Furthermore, for types that are :keep (which default to string), you should
add:

    --bar INTEGER (alias: -b) (may be given more than once)

And boolean ones accept no arguments, so they should be written as:

    --baz, --no-baz

Sort all of them alphabetically.
2025-07-26 19:56:05 +02:00
José Valim 99cef0686c Improve ExUnit docs 2025-07-26 19:56:05 +02:00
Paul Gideon Dann 2308be941e Validate type of :deps_paths option for formatter_for_file/2 (#14669) 2025-07-26 19:56:05 +02:00
Jean Klingler 79005e370e Fix opaqueness violation in Task.Supervisor (#14656) 2025-07-18 06:43:08 +09:00
José Valim 1f75ade71f Avoid adding lists that match negations 2025-07-16 22:09:44 +02:00
José Valim a719cb3b21 Update checker to v2 as representation has changed 2025-07-16 16:39:29 +02:00
Guillaume Duboc ac901d9d27 Remove duplicate for map_difference 2025-07-16 16:20:38 +02:00
Benjamin Milde f9966230ee Docs updates und restructuring (#14636) 2025-07-12 22:08:19 +02:00
Vasilis Spilka 9b729ca4c1 Add printable_limit and limit to IO.inspect doc examples (#14646) 2025-07-12 22:08:17 +02:00
Michał Łępicki 8cba20cb3e Clean up unreachable clause of Types.Descr.atom_only? helper (#14647)
it's being always called with a map
2025-07-12 22:08:10 +02:00
Jean Klingler b7e0d657d1 Drop :app_properties when rendering dependency in mix (#14645) 2025-07-12 21:11:21 +09:00
José Valim 2c3edbe1c7 Check for type equality 2025-07-11 16:15:53 +02:00
José Valim fec9899ead Revamp Mix & OTP guides (#14637) 2025-07-11 15:53:07 +02:00
Gary Rennie f54b192823 Add ETS to the Erlang Term Storage section of erlang libs (#14644)
This will ensure that it appears in the short search results on ExDoc
instead of having to navigate through to the search results.
2025-07-11 15:52:06 +02:00
Guillaume Duboc 855df4fc77 Domain keys in map (#14478)
- Introduced tests for union, intersection, and difference operations involving domain key types.
- Validated subtype relationships and intersection results for maps with domain keys.
- Enhanced map fetch and delete functionalities to handle domain key types.
- Ensured correct behavior of dynamic types with domain keys in various scenarios.
2025-07-11 15:52:06 +02:00
José Valim f87fbc2833 Clarify function types 2025-07-11 15:52:06 +02:00
José Valim 2b8ee38660 Tag / as an operator in fragments, closes #14643 2025-07-11 11:12:32 +02:00
José Valim 8d2775051d Update ... to an operator in Code.Fragment 2025-07-11 11:12:31 +02:00
Łukasz Samson 26855eda9f Update allow_local option spec (#14642) 2025-07-10 20:48:43 +02:00
José Valim ea77a68daa Add tests for allow_locals option 2025-07-10 17:32:08 +02:00
Łukasz Samson cb49bfb6e4 Add local_for_callback option to Macro.Env.expand_import (#14620) 2025-07-10 17:32:07 +02:00
Eksperimental c3ec400607 Use thin space (U+2009) as a separator instead of _ and in regular English language (#14635) 2025-07-10 10:22:11 +02:00
Eksperimental c03caa18cf Use backticks around literals in documentation (#14633) 2025-07-10 10:22:10 +02:00
José Valim bd68532c27 Remove explicit mentions to elixirc, as it isn't used in practice 2025-07-10 10:21:55 +02:00
Eksperimental a244323815 Correct grammar in structural sorting order section (#14639)
It is not required by the Elixir developer, but it is not required for them to know this by heart.

The former indicates that it is the Elixir developers who are not requiring this, the latter expresses
that they do not need to know this by heart.
2025-07-10 09:31:15 +02:00
Eksperimental fff2cf0d81 Standardize "Examples" heading section levels in docs (#14638)
* Convert "Examples" 3rd level headings to 2nd level when not under a 2nd level

* Convert 1st level "Examples" heading to 2nd level
2025-07-10 09:31:13 +02:00
José Valim b878f37577 Improve error message for protocols with no implementation, closes #14364 2025-07-10 09:30:42 +02:00
Jean Klingler 9619116cde Prevent mix test from overriding :failures_manifest_path option (#14632)
Introduced in 99be673
2025-07-08 16:08:16 +09:00
José Valim 248a71e2fc Fix logger docs
Closes #14628.
Closes #14629.
2025-07-06 09:46:35 +02:00
José Valim aa6964547d Fix return type of phi, closes #14621 2025-07-04 09:34:39 +02:00
José Valim 5c7687bf6f Apply further fn optimizations and fixes (#14619)
Closes #14598
2025-07-03 16:00:56 +02:00
Guillaume Duboc e73dd0c17d Perf optimizations and inferred intersections (#14605) 2025-07-03 16:00:48 +02:00
Guillaume Duboc f4037a31ba Simplified tuple definitions by removing negations (#14596) 2025-07-03 16:00:40 +02:00
José Valim a5b9c5b5db Add required field back to struct info
Closes #14616.
Closes #14617.
Closes #14500.
2025-07-03 12:22:25 +02:00
Łukasz Samson fcea4b4755 Handle filesystem errors in iex helpers (#14618)
`File.cd` and `File.ls` can return any posix error code
2025-07-03 10:29:41 +02:00
Michał Łępicki 24e63ccc47 Fix parallel option type in Mix.Compilers.Erlang.compile/6 spec (#14615)
compile.yecc and compile.leex tasks call it with parallel: true
2025-07-01 22:34:58 +02:00
Łukasz Samson d8bb1c4009 Add missing erlang compiler options (#14614)
Document options on leex and yecc compilers
2025-07-01 22:34:57 +02:00
José Valim fa2c96ffc3 Remove specs which are pass through and from private modules 2025-07-01 10:44:15 +02:00
Łukasz Samson db203167e2 Replace keyword with concrete keyword lists in specs (#14611) 2025-06-30 17:24:37 +02:00
José Valim a0ca37cb8f Fix warnings on Erlang/OTP 28 2025-06-30 11:57:26 +02:00
José Valim 592f44e2a8 Remove Regex warning until Erlang/OTP 28.1 2025-06-30 10:55:27 +02:00
José Valim 5ebe19e333 Make sure we log all output when partition fails 2025-06-27 15:30:41 +02:00
José Valim 9e3e817e42 Deal with undefined on :shell.whereis/0 2025-06-26 13:49:01 +02:00
José Valim a450d12912 Do not send quoted expressions to Macro.dbg 2025-06-26 13:38:17 +02:00
José Valim 65dbdef7dc Fix pry on Erlang/OTP 28 2025-06-26 13:32:57 +02:00
Jonatan Männchen 4f5746369b Use Workload Identity Federation for Windows Trusted Signing (#14604) 2025-06-25 19:31:00 +02:00
Steffen Deusch 274bc56f53 Add compilers option to Mix.install/2 (#14577) 2025-06-22 11:54:27 -07:00
José Valim 7d3047981c Distinguish source_anno from doc_anno, see #14595 2025-06-21 03:54:49 -07:00
José Valim 4f2868e632 Ensure block_keyword_or_binary_operator is handled in surround context, closes #14590 2025-06-21 03:25:44 -07:00
Łukasz Samson 41151190e5 Handle error result from unescape_tokens in tokenizer (#14587) 2025-06-19 12:02:26 -07:00
Łukasz Samson 711008a456 Consistently raise UnicodeConversionError in tokenizer (#14589) 2025-06-19 12:02:25 -07:00
Łukasz Samson 7a28203ff5 Fix invalid warning on no parens call on true (#14593) 2025-06-19 05:57:39 -07:00
José Valim f4bbf76d0b Optimize empty_difference_subtype? for dynamic parts 2025-06-14 12:13:19 -07:00
Jean Klingler 9fdb835f26 Mark inlined function call result as generated (#14581) 2025-06-14 19:07:36 +09:00
José Valim 95f982e004 Document bug fix on defstruct/defexception inside protocol, closes #14574 2025-06-12 11:36:47 +02:00
José Valim 7e08594ca0 Add tests for nested struct updates too 2025-06-11 17:05:24 +02:00
José Valim 2096f4156d Convert only the pattern matching suggestion into a hint 2025-06-11 14:08:07 +02:00
José Valim 9d845cf031 Update CHANGELOG 2025-06-11 13:59:26 +02:00
José Valim d74dff2bcf Transform the struct update syntax into a type assertion
This transforms the struct update into a type assertion,
requiring the type system to be sure the expression has
precisely the given struct type.

The struct update syntax may still be deprecated in the
future but this will provide a safer migration path and
allow us to engage in more conversations with the community.
2025-06-11 13:39:14 +02:00
José Valim 59a1ad9138 Allow captures to be reconstructed on type system pretty printing 2025-06-11 13:30:51 +02:00
José Valim 342c724863 Do no start listeners if --no-deps-check is given 2025-06-11 12:08:48 +02:00
José Valim 35818f1cdb Warn when invalid fun typespec is used 2025-06-11 10:25:24 +02:00
Joe Yates 502207c9f7 Fix use of prefer with '-ing' (#14568) 2025-06-10 11:17:13 +02:00
José Valim c3bc849550 Point out module must be required before macro usage in match/guard 2025-06-10 10:14:56 +02:00
José Valim f8de42a053 Filter @compile debug_info when explicitly set to true
Closes #14567.
2025-06-10 09:59:05 +02:00
José Valim ce33663780 Relax return type of impl_for with nil to avoid false positives 2025-06-10 09:49:55 +02:00
Tomasz Marek Sulima 752ca8864d Sort by call on tprof memory tests (Erlang/OTP 28) (#14565) 2025-06-09 22:27:08 +02:00
Theodor-Alexandru Irimia 8588a8d473 Clarify why and how to start second session for tests (#14566) 2025-06-09 22:27:08 +02:00
José Valim 2a9a4f2cab Release v1.19.0-rc.0 2025-06-09 12:27:46 +02:00
Michał Łępicki 0847e4b41a Fix mistake in "Untracked compile-time dependencies" anti-pattern (#14563) 2025-06-09 12:07:28 +02:00
José Valim 7aafd6ca77 Also download rebar3 automatically when compiling 2025-06-08 19:13:12 +02:00
José Valim 5ff421ca27 Disable inference as part of v1.19 release 2025-06-07 21:23:03 +02:00
José Valim 4425d684a0 Update CHANGELOG 2025-06-07 20:21:55 +02:00
José Valim e3f538cd25 Branch out v1.19 2025-06-07 20:18:21 +02:00
236 changed files with 13987 additions and 6390 deletions
+4 -4
View File
@@ -26,12 +26,12 @@ jobs:
fail-fast: false
matrix:
include:
- otp_version: "28.0"
- otp_version: "28.1"
deterministic: true
- otp_version: "28.0"
- otp_version: "28.1"
erlc_opts: "warnings_as_errors"
coverage: true
- otp_version: "28.0"
- otp_version: "28.1"
otp_latest: true
erlc_opts: "warnings_as_errors"
- otp_version: "27.3"
@@ -106,7 +106,7 @@ jobs:
name: Windows Server 2019, Erlang/OTP ${{ matrix.otp_version }}
strategy:
matrix:
otp_version: ["26.2", "27.3", "28.0"]
otp_version: ["26.2", "27.3", "28.1"]
runs-on: windows-2022
steps:
- name: Configure Git
+11 -1
View File
@@ -62,6 +62,16 @@ runs:
# Override Default Evaluator Rules
cp .ort/config/evaluator.rules.kts "$HOME/.ort/config/evaluator.rules.kts"
# Add Package Configurations
mkdir -p "$HOME/.ort/config/package-configurations/SpdxDocumentFile/The Elixir Team"
for FILE in .ort/package-configurations/*.yml; do
COMPONENT="$(basename "$FILE")"
cp "$FILE" "$HOME/.ort/config/package-configurations/SpdxDocumentFile/The Elixir Team/$COMPONENT"
sed -i -E \
"s/(\"SpdxDocumentFile:The Elixir Team:.+:)\"/\1${ELIXIR_VERSION}\"/" \
"$HOME/.ort/config/package-configurations/SpdxDocumentFile/The Elixir Team/$COMPONENT"
done
# Set Version in SPDX & Config
sed -i "s/# elixir-version-insert/versionInfo: '${ELIXIR_VERSION}'/" project.spdx.yml
sed -i -E "s/(\"SpdxDocumentFile:The Elixir Team:.+:)\"/\1${ELIXIR_VERSION}\"/" .ort.yml
@@ -80,7 +90,7 @@ runs:
id: ort
uses: oss-review-toolkit/ort-ci-github-action@1805edcf1f4f55f35ae6e4d2d9795ccfb29b6021 # v1.1.0
with:
image: ghcr.io/oss-review-toolkit/ort-minimal:54.0.0
image: ghcr.io/oss-review-toolkit/ort-minimal:65.0.0
run: >-
labels,
cache-dependencies,
+31 -21
View File
@@ -113,6 +113,7 @@ jobs:
sign:
needs: [build]
environment: release
strategy:
fail-fast: true
matrix:
@@ -126,6 +127,7 @@ jobs:
permissions:
contents: write
id-token: write
steps:
- name: "Download build"
@@ -133,23 +135,20 @@ jobs:
with:
name: build-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
- name: Log in to Azure
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: "Sign files with Trusted Signing"
uses: azure/trusted-signing-action@0d74250c661747df006298d0fb49944c10f16e03 # v0.5.1
if: github.repository == 'elixir-lang/elixir' && matrix.flavor == 'windows'
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
with:
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
# AZURE_TENANT_ID and AZURE_CLIENT_ID should stay the same,
# but AZURE_CLIENT_SECRET has expiration date. When it expires go to
# App Registrations / <app> / Certificates & secrets,
# click (+) New client secret, note the "Value" (not "Secret ID")
# and update it:
#
# $ gh --repo elixir-lang/elixir secret set AZURE_CLIENT_SECRET
azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
endpoint: https://eus.codesigning.azure.net/
trusted-signing-account-name: trusted-signing-elixir
certificate-profile-name: Elixir
trusted-signing-account-name: ${{ vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
certificate-profile-name: ${{ vars.AZURE_CERTIFICATE_PROFILE_NAME }}
files-folder: ${{ github.workspace }}
files-folder-filter: exe
file-digest: SHA256
@@ -304,16 +303,19 @@ jobs:
needs: [build, sign]
runs-on: ubuntu-22.04
concurrency: builds-hex-pm
environment: release
env:
AWS_ACCESS_KEY_ID: ${{ secrets.HEX_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.HEX_AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.HEX_AWS_REGION }}
AWS_S3_BUCKET: ${{ secrets.HEX_AWS_S3_BUCKET }}
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }}
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }}
FASTLY_KEY: ${{ secrets.HEX_FASTLY_KEY }}
OTP_GENERIC_VERSION: "25"
AWS_REGION: ${{ vars.HEX_AWS_REGION }}
AWS_S3_BUCKET: ${{ vars.HEX_AWS_S3_BUCKET }}
steps:
- name: "Check if variables are set up"
if: "${{ ! vars.HEX_AWS_REGION }}"
run: |
echo "Required variables for uploading to hex.pm are not set up, skipping..."
exit 1
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
pattern: "{sign-*-elixir-otp-*,Docs}"
@@ -327,6 +329,8 @@ jobs:
run: |
ref_name=${{ github.ref_name }}
oldest_otp=$(find . -type f -name 'elixir-otp-*.zip' | sed -r 's/^.*elixir-otp-([[:digit:]]+)\.zip$/\1/' | sort -n | head -n 1)
for zip in $(find . -type f -name 'elixir-otp-*.zip' | sed 's/^\.\///'); do
dest=${zip/elixir/${ref_name}}
surrogate_key=${dest/.zip$/}
@@ -336,7 +340,7 @@ jobs:
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${surrogate_key}\",\"surrogate-control\":\"public,max-age=604800\"}"
echo "builds/elixir/${surrogate_key}" >> purge_keys.txt
if [ "$zip" == "elixir-otp-${OTP_GENERIC_VERSION}.zip" ]; then
if [ "$zip" == "elixir-otp-${oldest_otp}.zip" ]; then
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${ref_name}.zip" \
--cache-control "public,max-age=3600" \
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${ref_name}\",\"surrogate-control\":\"public,max-age=604800\"}"
@@ -369,6 +373,8 @@ jobs:
date="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
ref_name=${{ github.ref_name }}
oldest_otp=$(find . -name 'elixir-otp-*.zip.sha256sum' | sed -r 's/^.*elixir-otp-([[:digit:]]+)\.zip\.sha256sum$/\1/' | sort -n | head -n 1)
aws s3 cp "s3://${AWS_S3_BUCKET}/builds/elixir/builds.txt" builds.txt || true
touch builds.txt
@@ -379,7 +385,7 @@ jobs:
sed -i "/^${ref_name}-${otp_version} /d" builds.txt
echo -e "${ref_name}-${otp_version} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
if [ "${otp_version}" == "otp-${OTP_GENERIC_VERSION}" ]; then
if [ "${otp_version}" == "otp-${oldest_otp}" ]; then
sed -i "/^${ref_name} /d" builds.txt
echo -e "${ref_name} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
fi
@@ -418,3 +424,7 @@ jobs:
for key in $(cat purge_keys.txt); do
purge "${key}"
done
env:
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }}
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }}
FASTLY_KEY: ${{ secrets.HEX_FASTLY_KEY }}
+56 -83
View File
@@ -3,18 +3,6 @@
excludes:
paths:
- pattern: "lib/elixir/pages/**/*"
reason: "DOCUMENTATION_OF"
comment: "Documentation"
- pattern: "lib/elixir/scripts/**/*"
reason: "BUILD_TOOL_OF"
comment: "Build Tool"
- pattern: "lib/ex_unit/examples/**/*"
reason: "EXAMPLE_OF"
comment: "Example"
- pattern: "lib/*/test/**/*"
reason: "TEST_OF"
comment: "Tests"
- pattern: "man/*"
reason: "DOCUMENTATION_OF"
comment: "Documentation"
@@ -25,8 +13,64 @@ excludes:
reason: "BUILD_TOOL_OF"
comment: "Documentation"
# Unfortunately we'll have to repeat all package level excludes here
# Make sure to keep them in sync with the package configuration in
# .ort/package-configurations
- pattern: "lib/*/pages/**/*"
reason: "DOCUMENTATION_OF"
comment: "Documentation"
- pattern: "lib/*/test/**/*"
reason: "TEST_OF"
comment: "Tests"
- pattern: "lib/*/scripts/**/*"
reason: "BUILD_TOOL_OF"
comment: "Build Tool"
- pattern: "lib/*/examples/**/*"
reason: "EXAMPLE_OF"
comment: "Example"
curations:
license_findings:
# Version File
- path: "VERSION"
reason: "NOT_DETECTED"
comment: "Apply Trademark Policy to VERSION file"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Wrongly Identified
- path: ".gitignore"
reason: "INCORRECT"
comment: "Ignored by ScanCode"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: ".gitattributes"
reason: "INCORRECT"
comment: "Ignored by ScanCode"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: "CONTRIBUTING.md"
reason: "INCORRECT"
comment: "Wrongly identified TSL license"
detected_license: "Apache-2.0 OR NOASSERTION OR LicenseRef-scancode-tsl-2020"
concluded_license: "Apache-2.0"
- path: "OPEN_SOURCE_POLICY.md"
reason: "INCORRECT"
comment: "Wrongly identified NOASSERTION"
detected_license: "NOASSERTION"
concluded_license: "Apache-2.0"
# Unfortunately we'll have to repeat all package level license curations here
# Make sure to keep them in sync with the package configuration in
# .ort/package-configurations
# Test Fixtures
- path: "lib/*/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Logos
- path: "lib/elixir/pages/images/logo.png"
reason: "NOT_DETECTED"
@@ -39,13 +83,6 @@ curations:
detected_license: "NONE"
concluded_license: "LicenseRef-elixir-trademark-policy"
# Version File
- path: "VERSION"
reason: "NOT_DETECTED"
comment: "Apply Trademark Policy to VERSION file"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Documentation Images
- path: "lib/elixir/pages/images/**/*.png"
reason: "NOT_DETECTED"
@@ -54,26 +91,11 @@ curations:
concluded_license: "Apache-2.0"
# Test Fixtures
- path: "lib/eex/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: "lib/elixir/test/elixir/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: "lib/ex_unit/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: "lib/mix/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Unicode
- path: "lib/elixir/unicode/*.txt"
@@ -89,57 +111,8 @@ curations:
The guide mentions multiple licenses for users to choose from.
It however is not licensed itself by the mentioned licenses.
concluded_license: "Apache-2.0"
- path: ".gitignore"
reason: "INCORRECT"
comment: "Ignored by ScanCode"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: ".gitattributes"
reason: "INCORRECT"
comment: "Ignored by ScanCode"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: "lib/elixir/scripts/windows_installer/.gitignore"
reason: "INCORRECT"
comment: "Ignored by ScanCode"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: "CONTRIBUTING.md"
reason: "INCORRECT"
comment: "Wrongly identified TSL license"
detected_license: "Apache-2.0 OR NOASSERTION OR LicenseRef-scancode-tsl-2020"
concluded_license: "Apache-2.0"
- path: "OPEN_SOURCE_POLICY.md"
reason: "INCORRECT"
comment: "Wrongly identified NOASSERTION"
detected_license: "NOASSERTION"
concluded_license: "Apache-2.0"
packages:
- id: "SpdxDocumentFile:The Elixir Team:elixir-lang:"
curations:
concluded_license: "Apache-2.0 AND LicenseRef-scancode-unicode"
- id: "SpdxDocumentFile:The Elixir Team:eex:"
curations:
concluded_license: "Apache-2.0"
is_metadata_only: true
- id: "SpdxDocumentFile:The Elixir Team:elixir:"
curations:
concluded_license: "Apache-2.0 AND LicenseRef-scancode-unicode"
is_metadata_only: true
- id: "SpdxDocumentFile:The Elixir Team:exunit:"
curations:
concluded_license: "Apache-2.0"
is_metadata_only: true
- id: "SpdxDocumentFile:The Elixir Team:iex:"
curations:
concluded_license: "Apache-2.0"
is_metadata_only: true
- id: "SpdxDocumentFile:The Elixir Team:logger:"
curations:
concluded_license: "Apache-2.0"
is_metadata_only: true
- id: "SpdxDocumentFile:The Elixir Team:mix:"
curations:
concluded_license: "Apache-2.0"
is_metadata_only: true
+8 -1
View File
@@ -3,6 +3,7 @@
ort:
enableRepositoryPackageCurations: true
enableRepositoryPackageConfigurations: true
scanner:
skipConcluded: false
@@ -11,4 +12,10 @@ ort:
analyzer:
allowDynamicVersions: true
enabledPackageManagers: [SpdxDocumentFile]
skipExcluded: true
reporter:
reporters:
SpdxDocument:
options:
creationInfoOrganization: The Elixir Team
documentName: "Elixir Source SPDX Document"
+15
View File
@@ -0,0 +1,15 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
id: "SpdxDocumentFile:The Elixir Team:eex:"
path_excludes:
- pattern: "lib/eex/test/**/*"
reason: "TEST_OF"
comment: "Tests"
license_finding_curations:
# Test Fixtures
- path: "lib/eex/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
+60
View File
@@ -0,0 +1,60 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
id: "SpdxDocumentFile:The Elixir Team:elixir:"
path_excludes:
- pattern: "lib/elixir/pages/**/*"
reason: "DOCUMENTATION_OF"
comment: "Documentation"
- pattern: "lib/elixir/scripts/**/*"
reason: "BUILD_TOOL_OF"
comment: "Build Tool"
- pattern: "lib/elixir/test/**/*"
reason: "TEST_OF"
comment: "Tests"
license_finding_curations:
# Logos
- path: "lib/elixir/pages/images/logo.png"
reason: "NOT_DETECTED"
comment: "Apply Trademark Policy to Elixir Logo"
detected_license: "NONE"
concluded_license: "LicenseRef-elixir-trademark-policy"
- path: "lib/elixir/scripts/windows_installer/assets/Elixir.ico"
reason: "NOT_DETECTED"
comment: "Apply Trademark Policy to Elixir Logo"
detected_license: "NONE"
concluded_license: "LicenseRef-elixir-trademark-policy"
# Documentation Images
- path: "lib/elixir/pages/images/**/*.png"
reason: "NOT_DETECTED"
comment: "Apply default license to all images"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Test Fixtures
- path: "lib/elixir/test/elixir/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Unicode
- path: "lib/elixir/unicode/*.txt"
reason: "NOT_DETECTED"
comment: "Apply default license to unicode files"
detected_license: "NONE"
concluded_license: "LicenseRef-scancode-unicode"
# Wrongly Identified
- path: "lib/elixir/pages/references/library-guidelines.md"
reason: "INCORRECT"
comment: |
The guide mentions multiple licenses for users to choose from.
It however is not licensed itself by the mentioned licenses.
concluded_license: "Apache-2.0"
- path: "lib/elixir/scripts/windows_installer/.gitignore"
reason: "INCORRECT"
comment: "Ignored by ScanCode"
detected_license: "NONE"
concluded_license: "Apache-2.0"
+18
View File
@@ -0,0 +1,18 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
id: "SpdxDocumentFile:The Elixir Team:exunit:"
path_excludes:
- pattern: "lib/ex_unit/examples/**/*"
reason: "EXAMPLE_OF"
comment: "Example"
- pattern: "lib/ex_unit/test/**/*"
reason: "TEST_OF"
comment: "Tests"
license_finding_curations:
# Test Fixtures
- path: "lib/ex_unit/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
+8
View File
@@ -0,0 +1,8 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
id: "SpdxDocumentFile:The Elixir Team:logger:"
path_excludes:
- pattern: "lib/logger/test/**/*"
reason: "TEST_OF"
comment: "Tests"
+15
View File
@@ -0,0 +1,15 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
id: "SpdxDocumentFile:The Elixir Team:mix:"
path_excludes:
- pattern: "lib/mix/test/**/*"
reason: "TEST_OF"
comment: "Tests"
license_finding_curations:
# Test Fixtures
- path: "lib/mix/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
+109 -36
View File
@@ -8,28 +8,6 @@
## Type system improvements
### More type inference
Elixir now performs inference of whole functions. The best way to show the new capabilities are with examples. Take the following code:
```elixir
def add_foo_and_bar(data) do
data.foo + data.bar
end
```
Elixir now infers that the function expects a `map` as first argument, and the map must have the keys `.foo` and `.bar` whose values are either `integer()` or `float()`. The return type will be either `integer()` or `float()`.
Here is another example:
```elixir
def sum_to_string(a, b) do
Integer.to_string(a + b)
end
```
Even though the `+` operator works with both integers and floats, Elixir infers that `a` and `b` must be both integers, as the result of `+` is given to a function that expects an integer. The inferred type information is then used during type checking to find possible typing errors.
### Type checking of protocol dispatch and implementations
This release also adds type checking when dispatching and implementing protocols.
@@ -144,13 +122,27 @@ While Elixir has always compiled the given files in project or a dependency in p
### Code loading bottlenecks
Prior to this release, Elixir would load modules as soon as they were defined. However, because the Erlang part of code loading happens within a single process (the code server), this would make it a bottleneck, reducing the amount of parallelization, especially on large projects.
Prior to this release, Elixir would load modules as soon as they were defined. However, because the Erlang part of code loading happens within a single process (the code server), this would make it a bottleneck, reducing parallelization, especially on large projects.
This release makes it so modules are loaded lazily. This reduces the pressure on the code server, making compilation up to 2x faster for large projects, and also reduces the overall amount of work done during compilation.
This release makes it so modules are loaded lazily. This reduces the pressure on the code server and the amount of work during compilation, with reports of more than two times faster compilation for large projects. The benefits depend on the codebase size and the number of CPU cores available.
Implementation wise, [the parallel compiler already acts as a mechanism to resolve modules during compilation](https://elixir-lang.org/blog/2012/04/24/a-peek-inside-elixir-s-parallel-compiler/), so we built on that. By making sure the compiler controls both module compilation and module loading, it can also better guarantee deterministic builds.
The only potential regression in this approach happens if you have a module, which is used at compile time and defines an `@on_load` callback (typically used for [NIFs](https://www.erlang.org/doc/system/nif.html)) that invokes another modules within the same project. For example:
There are two potential regressions with this approach. The first one happens if you spawn processes during compilation which invoke other modules defined within the same project. For example:
```elixir
defmodule MyLib.SomeModule do
list = [...]
Task.async_stream(list, fn item ->
MyLib.SomeOtherModule.do_something(item)
end)
end
```
Because the spawned process is not visible to the compiler, it won't be able to load `MyLib.SomeOtherModule`. You have two options, either use `Kernel.ParallelCompiler.pmap/2` or explicitly call `Code.ensure_compiled!(MyLib.SomeOtherModule)` before spawning the process that uses said module.
The second one is related to `@on_load` callbacks (typically used for [NIFs](https://www.erlang.org/doc/system/nif.html)) that invoke other modules defined within the same project. For example:
```elixir
defmodule MyLib.SomeModule do
@@ -170,11 +162,13 @@ MyLib.SomeModule.something_else()
The reason this fails is because `@on_load` callbacks are invoked within the code server and therefore they have limited ability to load additional modules. It is generally advisable to limit invocation of external modules during `@on_load` callbacks but, in case it is strictly necessary, you can set `@compile {:autoload, true}` in the invoked module to address this issue in a forward and backwards compatible manner.
Both snippets above could actually lead to non-deterministic compilation failures in the past, and as a result of these changes, compiling these cases are now deterministic.
### Parallel compilation of dependencies
This release introduces a variable called `MIX_OS_DEPS_COMPILE_PARTITION_COUNT`, which instructs `mix deps.compile` to compile dependencies in parallel.
While fetching dependencies and compiling individual Elixir dependencies already happened in parallel, there were pathological cases where performance would be left on the table, such as compiling dependencies with native code or dependencies where one or two large file would take over most of the compilation time.
While fetching dependencies and compiling individual Elixir dependencies already happened in parallel, as outlined in the previous section, there were pathological cases where performance gains would be left on the table, such as when compiling dependencies with native code or dependencies where one or two large files would take most of the compilation time.
By setting `MIX_OS_DEPS_COMPILE_PARTITION_COUNT` to a number greater than 1, Mix will now compile multiple dependencies at the same time, using separate OS processes. Empirical testing shows that setting it to half of the number of cores on your machine is enough to maximize resource usage. The exact speed up will depend on the number of dependencies and the number of machine cores, although some reports mention up to 4x faster compilation times. If you plan to enable it on CI or build servers, keep in mind it will most likely have a direct impact on memory usage too.
@@ -192,7 +186,7 @@ Elixir v1.19 ships with a new pretty printing implementation that tracks limits
]
```
This allows for more information to be shown at different nesting levels, which is useful for complex data structures. But it led to some pathological cases where the `limit` option had little effect on actually filtering the amount of data shown. The new implementation decouples the limit handling from depth, decreasing it as it goes. Therefore, the list above with the same limit in Elixir v1.19 is now printed as:
This allows for more information to be shown at different nesting levels, which is useful for complex data structures. But it led to some pathological cases where the `limit` option had little effect on filtering the amount of data shown. The new implementation decouples the limit handling from depth, decreasing it as it goes. Therefore, the list above with the same limit in Elixir v1.19 is now printed as:
```elixir
[
@@ -205,6 +199,30 @@ The outer list is the first element, the first nested list is the second, follow
Given this may reduce the amount of data printed by default, the default limit has also been increased from 50 to 100. We may further increase it in upcoming releases based on community feedback.
## Erlang/OTP 28 support
Elixir v1.19 officially supports Erlang/OTP 28.1+ and later. In order to support the new Erlang/OTP 28 representation for regular expressions, structs can now control how they are escaped into abstract syntax trees by defining a `__escape__/1` callback.
On the other hand, the new representation for regular expressions implies they can no longer be used as default values for struct fields. Instead of this:
```elixir
defmodule Foo do
defstruct regex: ~r/foo/
end
```
You must do this:
```elixir
defmodule Foo do
defstruct [:regex]
def new do
%Foo{regex: ~r/foo/}
end
end
```
## OpenChain certification
Elixir v1.19 is also our first release following OpenChain compliance, [as previously announced](https://elixir-lang.org/blog/2025/02/26/elixir-openchain-certification/). In a nutshell:
@@ -214,9 +232,29 @@ Elixir v1.19 is also our first release following OpenChain compliance, [as previ
These additions offer greater transparency into the components and licenses of each release, supporting more rigorous supply chain requirements.
This work was performed by Jonatan Männchen and sponsored by the Erlang Ecosystem Foundation.
This work was performed by [Jonatan Männchen](https://maennchen.dev) and sponsored by the [Erlang Ecosystem Foundation](https://erlef.org).
## v1.19.0-dev
## v1.19.1 (2025-10-20)
### 1. Bug fixes
#### EEx
* [EEx] Address Dialyzer warnings when invoking `EEx.compile_string`
#### Elixir
* [Kernel] Optimize how types are computed for pretty printing
* [Kernel] Optimize how differences are computed in the type system
* [Macro] Do not escape options given to `dbg/2`
* [Protocol] Improve protocol violation warnings
#### Mix
* [mix compile] Do not attempt to touch deleted files when compilation fails and then resumed with missing files
* [mix deps.compile] Do not spawn partitions when all dependencies are local and already compiled
## v1.19.0 (2025-10-16)
### 1. Enhancements
@@ -234,18 +272,26 @@ This work was performed by Jonatan Männchen and sponsored by the Erlang Ecosyst
* [Inspect] Allow `optional: :all` when deriving Inspect
* [Inspect.Algebra] Add optimistic/pessimistic groups as a simplified implementation of `next_break_fits`
* [IO.ANSI] Add ANSI codes to turn off conceal and crossed_out
* [Kernel] Allow controlling which applications are used during inference
* [Kernel] Raise when U+2028 and U+2029 characters are present in comments and strings to avoid line spoofing attacks
* [Kernel] Include the line for the previous clause in errors/warnings related to conflicts between defaults on function definitions
* [Kernel] Support `min/2` and `max/2` as guards
* [Kernel.ParallelCompiler] Add `each_long_verification_threshold` which invokes a callback when type checking a module takes too long
* [Kernel.ParallelCompiler] Include lines in `== Compilation error in file ... ==` slogans
* [Macro] Print debugging results from `Macro.dbg/3` as they happen, instead of once at the end
* [Macro] Add `__escape__/1` callback so structs can escape references and other runtime data types in `Macro.escape/1`
* [Module] Do not automatically load modules after their compilation, guaranteeing a more consistent compile time experience and drastically improving compilation times
* [OptionParser] Support the `:regex` type
* [OptionParser] Enhance parsing error to display available options
* [Protocol] Type checking of protocols dispatch and implementations
* [Regex] Add `Regex.to_embed/2` which returns an embeddable representation of regex in another regex
* [Regex] Raise error message when regexes are used as default values in struct fields for compatibility with Erlang/OTP 28
* [Registry] Add key-based partitioning of duplicate registries
* [String] Add `String.count/2` to count occurrences of a pattern
* [String] Update to Unicode 17.0.0
#### ExUnit
* [ExUnit] Set a process label for each test
* [ExUnit.CaptureLog] Parallelize log dispatch when multiple processes are capturing log
* [ExUnit.Case] Add `:test_group` to the test context
* [ExUnit.Doctest] Support ellipsis in doctest exceptions to match the remaining of the exception
@@ -256,35 +302,61 @@ This work was performed by Jonatan Männchen and sponsored by the Erlang Ecosyst
* [IEx] Support multi-line prompts (due to this feature, `:continuation_prompt` and `:alive_continuation_prompt` are no longer supported as IEx configuration)
* [IEx.Autocomplete] Functions annotated with `@doc group: "Name"` metadata will appear within their own groups in autocompletion
#### Logger
* [Logger] Accept any enumerable in `Logger.metadata/1`
#### Mix
* [mix] Add support for `MIX_PROFILE_FLAGS` to configure `MIX_PROFILE`
* [mix compile] Debug the compiler and type checker PID when `MIX_DEBUG=1` and compilation/verification thresholds are met
* [mix compile] Add `Mix.Tasks.Compiler.reenable/1`
* [mix deps.compile] Support `MIX_OS_DEPS_COMPILE_PARTITION_COUNT` for compiling deps concurrently across multiple operating system processes
* [mix help] Add `mix help Mod`, `mix help :mod`, `mix help Mod.fun` and `mix help Mod.fun/arity`
* [mix help] Add `mix help Mod`, `mix help :mod`, `mix help Mod.fun`, `mix help Mod.fun/arity`, and `mix help app:package`
* [mix format] Add options to mix format to allow excluding of files
* [mix test] Add `--name-pattern` option to `mix test`
* [mix test] Allow to distinguish the exit status between warnings as errors and test failures
* [mix xref graph] Add support for `--format json`
* [mix xref graph] Emit a warning if `--source` is part of a cycle
* [M ix.Task.Compiler] Add `Mix.Task.Compiler.run/2`
* [Mix] Support the `:compilers` option
* [Mix.Task.Compiler] Add `Mix.Task.Compiler.run/2`
### 2. Bug fixes
#### Elixir
* [Code] Return error on invalid unicode sequences in `Code.string_to_quoted/2` instead of raising
* [Code] Properly handle column annotation for `in` in `not in` expressions
* [DateTime] Do not truncate microseconds regardless of precision in `DateTime.diff/3`
* [Enum] Fix infinite loop on `Enum.take/2` with negative index on empty enumerable
* [File] Properly handle permissions errors cascading from parent in `File.mkdir_p/1`
* [Inspect] Inspect ill-formed structs as maps
* [Kernel] Properly increment metadata newline when `?` is followed by a literal newline character
* [Kernel] `not_a_map.key` now raises `BadMapError` for consistency with other map operations
* [Protocol] `defstruct/1` and `defexception/1` are now disabled inside `defprotocol` as to not allow defining structs/exceptions alongside a protocol
* [Regex] Fix `Regex.split/2` returning too many results when the chunk being split on was empty (which can happen when using features such as `/K`)
* [Stream] Ensure `Stream.transform/5` respects suspend command when its inner stream halts
* [URI] Several fixes to `URI.merge/2` related to trailing slashes, trailing dots, and hostless base URIs
#### ExUnit
* [ExUnit.Assertions] Fix order of pinned variables in failure reports
* [ExUnit.Assertions] Raise if attempting to raise an assertion error with invalid message (not a binary)
* [ExUnit.Case] Do not crash on empty test unit groups
#### IEx
* [IEx] Abort pipelines when there is an error in any step along the way
#### Mix
* [mix cmd] Preserve argument quoting in subcommands
* [mix cmd] Preserve argument quoting in subcommands by no longer performing shell expansion. To revert to the previous behaviour, pass `--shell` before the command name
* [mix compile] Fix bug where reverting changes to an external resource (such as HEEx template) after a compilation error would make it so the source module would not be compiled
* [mix compile] Avoid failures when locking compilation across different users
* [mix compile] Fix race condition when renaming files used by the compilation lock
* [mix format] Ensure the formatter does not go over the specified limit in certain corner cases
* [mix release] Fix `RELEASE_SYS_CONFIG` for Windows 11
* [mix test] Preserve files with no longer filter on `mix test`
* [mix test] Ensure modules are preloaded in `mix test --slowest-modules=N`
* [mix xref graph] Provide more consistent output by considering strong connected components only when computing graphs
### 3. Soft deprecations (no warnings emitted)
@@ -297,16 +369,17 @@ This work was performed by Jonatan Männchen and sponsored by the Erlang Ecosyst
#### Mix
* [mix compile] `--no-protocol-consolidation` is deprecated in favor of `--no-consolidate-protocols` for consistency with `mix.exs` configuration
* [mix compile.protocols] Protocol consolidation is now part of `compile.elixir` and has no effect
* [mix compile.protocols] Protocol consolidation is now part of `compile.elixir` and the task itself has no effect
### 4. Hard deprecations
#### Elixir
* [Code] Warn if line-break characters outside of `\r` and `\r\n` are found in strings according to UX#55. This warning will be fast-tracked into an error for security reasons in Elixir v1.20, following a similar rule to bidirectional control characters. They will already raise if found in comments
* [Code] The `on_undefined_variable: :warn` is deprecated. Relying on undefined variables becoming function calls will not be supported in the future
* [File] Passing a callback as third argument to `File.cp/3` is deprecated, pass it as a `on_conflict: callback` option instead
* [File] Passing a callback as third argument to `File.cp_r/3` is deprecated, pass it as a `on_conflict: callback` option instead
* [Kernel] The struct update syntax, such as `%URI{uri | path: "/foo/bar"}` is deprecated in favor of pattern matching on the struct when the variable is defined and then using the map update syntax `%{uri | path: "/foo/bar"}`. Thanks to the type system, pattern matching on structs can find more errors, more reliably
* [Kernel] The struct update syntax, such as `%URI{uri | path: "/foo/bar"}`, now requires the given variable (or expression) to explicitly pattern match on the struct before it can be updated. This is because, thanks to the type system, pattern matching on structs can find more errors, more reliably, and we want to promote its usage. Once pattern matching is added, you may optionally convert the struct update syntax into the map update syntax `%{uri | path: "/foo/bar"}` with no less of typing guarantees
* [Kernel.ParallelCompiler] Passing `return_diagnostics: true` as an option is required on `compile`, `compile_to_path` and `require`
#### Logger
+1 -1
View File
@@ -6,7 +6,7 @@ PREFIX ?= /usr/local
TEST_FILES ?= "*_test.exs"
SHARE_PREFIX ?= $(PREFIX)/share
MAN_PREFIX ?= $(SHARE_PREFIX)/man
CANONICAL := main/
# CANONICAL := main/
ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS)
ELIXIRC_MIN_SIG := $(ELIXIRC) -e 'Code.put_compiler_option :infer_signatures, []'
ERLC := erlc -I lib/elixir/include
+2 -8
View File
@@ -8,15 +8,9 @@
## Shipping a new version
1. Update version in /VERSION, bin/elixir, bin/elixir.bat, and bin/elixir.ps1
1. Update version in /VERSION, bin/elixir, and bin/elixir.bat
2. Ensure /CHANGELOG.md is updated, versioned and add the current date
- If this release addresses any publicly known security vulnerabilities with
assigned CVEs, add a "Security" section to `CHANGELOG.md`. For example:
```md
## Security
- Fixed CVE-2025-00000: Description of the vulnerability
```
3. Update "Compatibility and Deprecations" if a new OTP version is supported
@@ -34,7 +28,7 @@
### In the new branch
1. Comment the `CANONICAL=` in /Makefile
1. Comment out `CANONICAL := main/` in /Makefile
2. Update tables in /SECURITY.md and "Compatibility and Deprecations"
+2 -3
View File
@@ -12,12 +12,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
Elixir version | Support
:------------- | :-----------------------------
1.19 | Development
1.18 | Bug fixes and security patches
1.19 | Bug fixes and security patches
1.18 | Security patches only
1.17 | Security patches only
1.16 | Security patches only
1.15 | Security patches only
1.14 | Security patches only
## Announcements
+1 -1
View File
@@ -1 +1 @@
1.19.0-dev
1.19.1
+1 -1
View File
@@ -6,7 +6,7 @@
set -e
ELIXIR_VERSION=1.19.0-dev
ELIXIR_VERSION=1.19.1
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
cat <<USAGE >&2
+1 -1
View File
@@ -4,7 +4,7 @@
:: SPDX-FileCopyrightText: 2021 The Elixir Team
:: SPDX-FileCopyrightText: 2012 Plataformatec
set ELIXIR_VERSION=1.19.0-dev
set ELIXIR_VERSION=1.19.1
if ""%1""=="""" if ""%2""=="""" goto documentation
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
+22 -6
View File
@@ -118,6 +118,19 @@ defmodule EEx do
| {:expr | :start_expr | :middle_expr | :end_expr, marker, charlist, metadata}
| {:eof, metadata}
@type tokenize_opt ::
{:file, binary()}
| {:line, line}
| {:column, column}
| {:indentation, non_neg_integer}
| {:trim, boolean()}
@type compile_opt ::
tokenize_opt
| {:engine, module()}
| {:parser_options, Code.parser_opts()}
| {atom(), term()}
@doc """
Generates a function definition from the given string.
@@ -128,6 +141,7 @@ defmodule EEx do
template.
The supported `options` are described [in the module docs](#module-options).
Additional options are passed to the underlying engine.
## Examples
@@ -220,9 +234,11 @@ defmodule EEx do
"3"
"""
@spec compile_string(String.t(), keyword) :: Macro.t()
@spec compile_string(String.t(), [compile_opt]) :: Macro.t()
def compile_string(source, options \\ []) when is_binary(source) and is_list(options) do
case tokenize(source, options) do
tokenize_opts = Keyword.take(options, [:file, :line, :column, :indentation, :trim])
case tokenize(source, tokenize_opts) do
{:ok, tokens} ->
EEx.Compiler.compile(tokens, source, options)
@@ -259,7 +275,7 @@ defmodule EEx do
#=> "3"
"""
@spec compile_file(Path.t(), keyword) :: Macro.t()
@spec compile_file(Path.t(), [compile_opt]) :: Macro.t()
def compile_file(filename, options \\ []) when is_list(options) do
filename = IO.chardata_to_string(filename)
options = Keyword.merge([file: filename, line: 1], options)
@@ -277,7 +293,7 @@ defmodule EEx do
"foo baz"
"""
@spec eval_string(String.t(), keyword, keyword) :: String.t()
@spec eval_string(String.t(), keyword, [compile_opt]) :: String.t()
def eval_string(source, bindings \\ [], options \\ [])
when is_binary(source) and is_list(bindings) and is_list(options) do
compiled = compile_string(source, options)
@@ -299,7 +315,7 @@ defmodule EEx do
#=> "foo baz"
"""
@spec eval_file(Path.t(), keyword, keyword) :: String.t()
@spec eval_file(Path.t(), keyword, [compile_opt]) :: String.t()
def eval_file(filename, bindings \\ [], options \\ [])
when is_list(bindings) and is_list(options) do
filename = IO.chardata_to_string(filename)
@@ -339,7 +355,7 @@ defmodule EEx do
Note new tokens may be added in the future.
"""
@doc since: "1.14.0"
@spec tokenize([char()] | String.t(), opts :: keyword) ::
@spec tokenize([char()] | String.t(), [tokenize_opt]) ::
{:ok, [token()]} | {:error, String.t(), metadata()}
def tokenize(contents, opts \\ []) do
EEx.Compiler.tokenize(contents, opts)
+4
View File
@@ -17,6 +17,10 @@ defmodule EEx.Engine do
@doc """
Called at the beginning of every template.
It receives the options during compilation, including the
ones managed by EEx, such as `:line` and `:file`, as well
as custom engine options.
It must return the initial state.
"""
@callback init(opts :: keyword) :: state
+1 -1
View File
@@ -891,7 +891,7 @@ defmodule Access do
end
defp filter(:get, data, func, next) when is_list(data) do
data |> Enum.filter(func) |> Enum.map(next)
for elem <- data, func.(elem), do: next.(elem)
end
defp filter(:get_and_update, data, func, next) when is_list(data) do
+17 -1
View File
@@ -162,6 +162,22 @@ defmodule Calendar do
"""
@type time_zone_database :: module()
@typedoc """
Options for formatting dates and times with `strftime/3`.
"""
@type strftime_opts :: [
preferred_datetime: String.t(),
preferred_date: String.t(),
preferred_time: String.t(),
am_pm_names: (:am | :pm -> String.t()) | (:am | :pm, map() -> String.t()),
month_names: (pos_integer() -> String.t()) | (pos_integer(), map() -> String.t()),
abbreviated_month_names:
(pos_integer() -> String.t()) | (pos_integer(), map() -> String.t()),
day_of_week_names: (pos_integer() -> String.t()) | (pos_integer(), map() -> String.t()),
abbreviated_day_of_week_names:
(pos_integer() -> String.t()) | (pos_integer(), map() -> String.t())
]
@doc """
Returns how many days there are in the given month of the given year.
"""
@@ -617,7 +633,7 @@ defmodule Calendar do
"""
@doc since: "1.11.0"
@spec strftime(map(), String.t(), keyword()) :: String.t()
@spec strftime(map(), String.t(), strftime_opts()) :: String.t()
def strftime(date_or_time_or_datetime, string_format, user_options \\ [])
when is_map(date_or_time_or_datetime) and is_binary(string_format) do
parse(
+12 -7
View File
@@ -321,7 +321,7 @@ defmodule Date do
@doc """
Converts the given date to a string according to its calendar.
### Examples
## Examples
iex> Date.to_string(~D[2000-02-28])
"2000-02-28"
@@ -399,7 +399,7 @@ defmodule Date do
or other calendars in which the days also start at midnight.
Attempting to convert dates from other calendars will raise an `ArgumentError`.
### Examples
## Examples
iex> Date.to_iso8601(~D[2000-02-28])
"2000-02-28"
@@ -633,7 +633,7 @@ defmodule Date do
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
year:
iex> Date.convert(~D[2000-01-01], Calendar.Holocene)
@@ -667,7 +667,7 @@ defmodule Date do
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
year:
iex> Date.convert!(~D[2000-01-01], Calendar.Holocene)
@@ -691,10 +691,15 @@ defmodule Date do
@doc """
Adds the number of days to the given `date`.
The days are counted as Gregorian days. The date is returned in the same
calendar as it was given in.
> #### Prefer `shift/2` {: .info}
>
> Prefer `shift/2` over `add/2`, as it offers a more ergonomic API.
>
> `add/2` always considers a day to be measured according to the
> `Calendar.ISO`.
To shift a date by a `Duration` and according to its underlying calendar, use `Date.shift/2`.
The days are counted as Gregorian days, independent of the underlying
calendar. The date is returned in the same calendar as it was given in.
## Examples
+1 -1
View File
@@ -95,7 +95,7 @@ defmodule Date.Range do
[date_from_iso_days(current, calendar)]
end
defp slice(current, step, remaining, calendar) do
defp slice(current, step, remaining, calendar) when remaining > 1 do
[
date_from_iso_days(current, calendar)
| slice(current + step, step, remaining - 1, calendar)
+41 -21
View File
@@ -1046,7 +1046,7 @@ defmodule DateTime do
its abbreviation, which means information is lost when converting to such
format.
### Examples
## Examples
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
@@ -1390,7 +1390,7 @@ defmodule DateTime do
custom (but relatively common) representation which appends the time
zone abbreviation and full name to the datetime.
### Examples
## Examples
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
@@ -1611,32 +1611,44 @@ defmodule DateTime do
@doc """
Adds a specified amount of time to a `DateTime`.
> #### Prefer `shift/2` {: .info}
>
> Prefer `shift/2` over `add/3`, as it offers a more ergonomic API.
>
> `add/3` provides a lower-level API which only supports fixed units
> such as `:hour` and `:second`, but not `:month` (as the exact length
> of a month depends on the current month). `add/3` always considers
> the unit to be computed according to the `Calendar.ISO`.
Accepts an `amount_to_add` in any `unit`. `unit` can be `:day`,
`:hour`, `:minute`, `:second` or any subsecond precision from
`t:System.time_unit/0`. It defaults to `:second`. Negative values
will move backwards in time.
This function always considers the unit to be computed according
to the `Calendar.ISO`.
This function relies on a contiguous representation of time,
ignoring the wall time and timezone changes. For example, if you add
one day when there are summer time/daylight saving time changes,
it will also change the time forward or backward by one hour,
so the elapsed time is precisely 24 hours. Similarly, adding just
a few seconds to a datetime just before "spring forward" can cause
wall time to increase by more than an hour.
ignoring timezone changes. For example, if you add one day when there
are summer time/daylight saving time changes, it will also change the
time forward or backward by one hour, so the elapsed time is precisely
24 hours. Similarly, adding just a few seconds to a datetime just before
"spring forward" can cause wall time to increase by more than an hour.
While this means this function is precise in terms of elapsed time,
its result may be misleading in certain use cases. For example, if a
its result may be confusing in certain use cases. For example, if a
user requests a meeting to happen every day at 15:00 and you use this
function to compute all future meetings by adding day after day, this
function may change the meeting time to 14:00 or 16:00 if there are
changes to the current timezone. Computing of recurring datetimes is
not currently supported in Elixir's standard library but it is available
by third-party libraries.
changes to the current timezone.
### Examples
In case you don't want these changes to happen automatically or you
want to surface time zone conflicts to the user, you can add to
the datetime as a naive datetime and then use `from_naive/2`:
dt |> NaiveDateTime.add(1, :day) |> DateTime.from_naive(dt.time_zone)
The above will surface time jumps and ambiguous datetimes, allowing you
to deal with them accordingly.
## Examples
iex> dt = DateTime.from_naive!(~N[2018-11-15 10:00:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
iex> dt |> DateTime.add(3600, :second, FakeTimeZoneDatabase)
@@ -1664,8 +1676,6 @@ defmodule DateTime do
iex> result.microsecond
{21000, 3}
To shift a datetime by a `Duration` and according to its underlying calendar, use `DateTime.shift/3`.
"""
@doc since: "1.8.0"
@spec add(
@@ -1739,7 +1749,7 @@ defmodule DateTime do
to UTC, and finally computing the new timezone in case of shifts.
This ensures `shift/3` always returns a valid datetime.
On the other hand, time zones that observe "Daylight Saving Time"
Consequently, time zones that observe "Daylight Saving Time"
or other changes, across summer/winter time will add/remove hours
from the resulting datetime:
@@ -1751,12 +1761,22 @@ defmodule DateTime do
DateTime.shift(dt, hour: 2)
#=> #DateTime<2018-11-04 01:00:00-08:00 PST America/Los_Angeles>
Although the first example shows a difference of 2 hours when
comparing the wall clocks of the given datetime with the returned one,
due to the "spring forward" time jump, the actual elapsed time is
still exactly of 1 hour.
In case you don't want these changes to happen automatically or you
want to surface time zone conflicts to the user, you can shift
the datetime as a naive datetime and then use `from_naive/2`:
dt |> NaiveDateTime.shift(duration) |> DateTime.from_naive(dt.time_zone)
The above will surface time jumps and ambiguous datetimes, allowing you
to deal with them accordingly.
## ISO calendar considerations
When using the default ISO calendar, durations are collapsed and
applied in the order of months, then seconds and microseconds:
@@ -1922,7 +1942,7 @@ defmodule DateTime do
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
year:
iex> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
@@ -1969,7 +1989,7 @@ defmodule DateTime do
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
year:
iex> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
+17
View File
@@ -161,6 +161,22 @@ defmodule Duration do
"""
@type duration :: t | [unit_pair]
@typedoc """
Options for `Duration.to_string/2`.
"""
@type to_string_opts :: [
units: [
year: String.t(),
month: String.t(),
week: String.t(),
day: String.t(),
hour: String.t(),
minute: String.t(),
second: String.t()
],
separator: String.t()
]
@microseconds_per_second 1_000_000
@doc """
@@ -436,6 +452,7 @@ defmodule Duration do
"""
@doc since: "1.18.0"
@spec to_string(t, to_string_opts) :: String.t()
def to_string(%Duration{} = duration, opts \\ []) do
units = Keyword.get(opts, :units, [])
separator = Keyword.get(opts, :separator, " ")
+13 -9
View File
@@ -391,14 +391,20 @@ defmodule NaiveDateTime do
@doc """
Adds a specified amount of time to a `NaiveDateTime`.
> #### Prefer `shift/2` {: .info}
>
> Prefer `shift/2` over `add/3`, as it offers a more ergonomic API.
>
> `add/3` provides a lower-level API which only supports fixed units
> such as `:hour` and `:second`, but not `:month` (as the exact length
> of a month depends on the current month). `add/3` always considers
> the unit to be computed according to the `Calendar.ISO`.
Accepts an `amount_to_add` in any `unit`. `unit` can be `:day`,
`:hour`, `:minute`, `:second` or any subsecond precision from
`t:System.time_unit/0`. It defaults to `:second`. Negative values
will move backwards in time.
This function always consider the unit to be computed according
to the `Calendar.ISO`.
## Examples
It uses seconds by default:
@@ -447,8 +453,6 @@ defmodule NaiveDateTime do
iex> NaiveDateTime.add(dt, 21, :second)
~N[2000-02-29 23:00:28]
To shift a naive datetime by a `Duration` and according to its underlying calendar, use `NaiveDateTime.shift/2`.
"""
@doc since: "1.4.0"
@spec add(Calendar.naive_datetime(), integer, :day | :hour | :minute | System.time_unit()) :: t
@@ -761,7 +765,7 @@ defmodule NaiveDateTime do
For readability, this function follows the RFC3339 suggestion of removing
the "T" separator between the date and time components.
### Examples
## Examples
iex> NaiveDateTime.to_string(~N[2000-02-28 23:00:13])
"2000-02-28 23:00:13"
@@ -908,7 +912,7 @@ defmodule NaiveDateTime do
Only supports converting naive datetimes which are in the ISO calendar,
attempting to convert naive datetimes from other calendars will raise.
### Examples
## Examples
iex> NaiveDateTime.to_iso8601(~N[2000-02-28 23:00:13])
"2000-02-28T23:00:13"
@@ -1261,7 +1265,7 @@ defmodule NaiveDateTime do
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
year:
iex> NaiveDateTime.convert(~N[2000-01-01 13:30:15], Calendar.Holocene)
@@ -1327,7 +1331,7 @@ defmodule NaiveDateTime do
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
year:
iex> NaiveDateTime.convert!(~N[2000-01-01 13:30:15], Calendar.Holocene)
+11 -9
View File
@@ -225,7 +225,7 @@ defmodule Time do
@doc """
Converts the given `time` to a string.
### Examples
## Examples
iex> Time.to_string(~T[23:00:00])
"23:00:00"
@@ -334,7 +334,7 @@ defmodule Time do
format, for human readability. It also supports the "basic" format through
passing the `:basic` option.
### Examples
## Examples
iex> Time.to_iso8601(~T[23:00:13])
"23:00:13"
@@ -505,14 +505,18 @@ defmodule Time do
@doc """
Adds the `amount_to_add` of `unit`s to the given `time`.
> #### Prefer `shift/2` {: .info}
>
> Prefer `shift/2` over `add/3`, as it offers a more ergonomic API.
>
> `add/3` always considers the unit to be computed according to
> the `Calendar.ISO`.
Accepts an `amount_to_add` in any `unit`. `unit` can be
`:hour`, `:minute`, `:second` or any subsecond precision from
`t:System.time_unit/0`. It defaults to `:second`. Negative values
will move backwards in time.
This function always consider the unit to be computed according
to the `Calendar.ISO`.
Note the result value represents the time of day, meaning that it is cyclic,
for instance, it will never go over 24 hours for the ISO calendar.
@@ -549,8 +553,6 @@ defmodule Time do
iex> result.microsecond
{21000, 3}
To shift a time by a `Duration` and according to its underlying calendar, use `Time.shift/2`.
"""
@doc since: "1.6.0"
@spec add(Calendar.time(), integer, :hour | :minute | System.time_unit()) :: t
@@ -781,7 +783,7 @@ defmodule Time do
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
year:
iex> Time.convert(~T[13:30:15], Calendar.Holocene)
@@ -837,7 +839,7 @@ defmodule Time do
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
year:
iex> Time.convert!(~T[13:30:15], Calendar.Holocene)
+84 -29
View File
@@ -248,6 +248,58 @@ defmodule Code do
"""
@type position() :: line() | {line :: pos_integer(), column :: pos_integer()}
@typedoc """
Options for code formatting functions.
"""
@type format_opt ::
{:file, binary()}
| {:line, pos_integer()}
| {:line_length, pos_integer()}
| {:locals_without_parens, keyword()}
| {:force_do_end_blocks, boolean()}
| {:migrate, boolean()}
| {:migrate_bitstring_modifiers, boolean()}
| {:migrate_call_parens_on_pipe, boolean()}
| {:migrate_charlists_as_sigils, boolean()}
| {:migrate_unless, boolean()}
| {atom(), term()}
@typedoc """
Options for `quoted_to_algebra/2`.
"""
@type quoted_to_algebra_opt ::
{:line, pos_integer() | nil}
| {:escape, boolean()}
| {:locals_without_parens, keyword()}
| {:comments, [term()]}
@typedoc """
Options for parsing functions that convert strings to quoted expressions.
"""
@type parser_opts :: [
file: binary(),
line: pos_integer(),
column: pos_integer(),
indentation: non_neg_integer(),
columns: boolean(),
unescape: boolean(),
existing_atoms_only: boolean(),
token_metadata: boolean(),
literal_encoder: (term(), Macro.metadata() -> term()),
static_atoms_encoder: (atom() -> term()),
emit_warnings: boolean()
]
@typedoc """
Options for environment evaluation functions like eval_string/3 and eval_quoted/3.
"""
@type env_eval_opts :: [
file: binary(),
line: pos_integer(),
module: module(),
prune_binding: boolean()
]
@boolean_compiler_options [
:docs,
:debug_info,
@@ -560,7 +612,7 @@ defmodule Code do
[a: 1, b: 2]
"""
@spec eval_string(List.Chars.t(), binding, Macro.Env.t() | keyword) :: {term, binding}
@spec eval_string(List.Chars.t(), binding, Macro.Env.t() | env_eval_opts) :: {term, binding}
def eval_string(string, binding \\ [], opts \\ [])
def eval_string(string, binding, %Macro.Env{} = env) do
@@ -615,7 +667,8 @@ defmodule Code do
"""
@doc since: "1.15.0"
@spec with_diagnostics(keyword(), (-> result)) :: {result, [diagnostic(:warning | :error)]}
@spec with_diagnostics([log: boolean()], (-> result)) ::
{result, [diagnostic(:warning | :error)]}
when result: term()
def with_diagnostics(opts \\ [], fun) do
value = :erlang.get(:elixir_code_diagnostics)
@@ -648,7 +701,7 @@ defmodule Code do
Defaults to `true`.
"""
@doc since: "1.15.0"
@spec print_diagnostic(diagnostic(:warning | :error), keyword()) :: :ok
@spec print_diagnostic(diagnostic(:warning | :error), snippet: boolean()) :: :ok
def print_diagnostic(diagnostic, opts \\ []) do
read_snippet? = Keyword.get(opts, :snippet, true)
:elixir_errors.print_diagnostic(diagnostic, read_snippet?)
@@ -672,7 +725,7 @@ defmodule Code do
* `:line` - the line the string starts, used for error reporting
* `:line_length` - the line length to aim for when formatting
the document. Defaults to 98. This value indicates when an expression
the document. Defaults to `98`. This value indicates when an expression
should be broken over multiple lines but it is not guaranteed
to do so. See the "Line length" section below for more information
@@ -1035,7 +1088,7 @@ defmodule Code do
address the deprecation warnings.
"""
@doc since: "1.6.0"
@spec format_string!(binary, keyword) :: iodata
@spec format_string!(binary, [format_opt]) :: iodata
def format_string!(string, opts \\ []) when is_binary(string) and is_list(opts) do
line_length = Keyword.get(opts, :line_length, 98)
@@ -1060,7 +1113,7 @@ defmodule Code do
available options.
"""
@doc since: "1.6.0"
@spec format_file!(binary, keyword) :: iodata
@spec format_file!(binary, [format_opt]) :: iodata
def format_file!(file, opts \\ []) when is_binary(file) and is_list(opts) do
string = File.read!(file)
formatted = format_string!(string, [file: file, line: 1] ++ opts)
@@ -1098,7 +1151,7 @@ defmodule Code do
[a: 1, b: 2]
"""
@spec eval_quoted(Macro.t(), binding, Macro.Env.t() | keyword) :: {term, binding}
@spec eval_quoted(Macro.t(), binding, Macro.Env.t() | env_eval_opts) :: {term, binding}
def eval_quoted(quoted, binding \\ [], env_or_opts \\ []) do
{value, binding, _env} =
eval_verify(:eval_quoted, [quoted, binding, env_for_eval(env_or_opts)])
@@ -1129,8 +1182,15 @@ defmodule Code do
* `:line` - the line on which the script starts
* `:module` - the module to run the environment on
* `:prune_binding` - (since v1.14.2) prune binding to keep only
variables read or written by the evaluated code. Note that
variables used by modules are always pruned, even if later used
by the modules. You can submit to the `:on_module` tracer event
and access the variables used by the module from its environment.
"""
@doc since: "1.14.0"
@spec env_for_eval(Macro.Env.t() | env_eval_opts) :: Macro.Env.t()
def env_for_eval(env_or_opts), do: :elixir.env_for_eval(env_or_opts)
@doc """
@@ -1144,15 +1204,11 @@ defmodule Code do
## Options
* `:prune_binding` - (since v1.14.2) prune binding to keep only
variables read or written by the evaluated code. Note that
variables used by modules are always pruned, even if later used
by the modules. You can submit to the `:on_module` tracer event
and access the variables used by the module from its environment.
It accepts the same options as `env_for_eval/1`.
"""
@doc since: "1.14.0"
@spec eval_quoted_with_env(Macro.t(), binding, Macro.Env.t(), keyword) ::
@spec eval_quoted_with_env(Macro.t(), binding, Macro.Env.t(), env_eval_opts) ::
{term, binding, Macro.Env.t()}
def eval_quoted_with_env(quoted, binding, %Macro.Env{} = env, opts \\ [])
when is_list(binding) do
@@ -1171,14 +1227,14 @@ defmodule Code do
Defaults to `"nofile"`.
* `:line` - the starting line of the string being parsed.
Defaults to 1.
Defaults to `1`.
* `:column` - (since v1.11.0) the starting column of the string being parsed.
Defaults to 1.
Defaults to `1`.
* `:indentation` - (since v1.19.0) the indentation for the string being parsed.
This is useful when the code parsed is embedded within another document.
Defaults to 0.
Defaults to `0`.
* `:columns` - when `true`, attach a `:column` key to the quoted
metadata. Defaults to `false`.
@@ -1263,7 +1319,7 @@ defmodule Code do
{:error, {[line: 1, column: 4], "syntax error before: ", "\"3\""}}
"""
@spec string_to_quoted(List.Chars.t(), keyword) ::
@spec string_to_quoted(List.Chars.t(), parser_opts) ::
{:ok, Macro.t()} | {:error, {location :: keyword, binary | {binary, binary}, binary}}
def string_to_quoted(string, opts \\ []) when is_list(opts) do
file = Keyword.get(opts, :file, "nofile")
@@ -1290,7 +1346,7 @@ defmodule Code do
Check `string_to_quoted/2` for options information.
"""
@spec string_to_quoted!(List.Chars.t(), keyword) :: Macro.t()
@spec string_to_quoted!(List.Chars.t(), parser_opts) :: Macro.t()
def string_to_quoted!(string, opts \\ []) when is_list(opts) do
file = Keyword.get(opts, :file, "nofile")
line = Keyword.get(opts, :line, 1)
@@ -1341,7 +1397,7 @@ defmodule Code do
"""
@doc since: "1.13.0"
@spec string_to_quoted_with_comments(List.Chars.t(), keyword) ::
@spec string_to_quoted_with_comments(List.Chars.t(), parser_opts) ::
{:ok, Macro.t(), list(map())} | {:error, {location :: keyword, term, term}}
def string_to_quoted_with_comments(string, opts \\ []) when is_list(opts) do
charlist = to_charlist(string)
@@ -1371,7 +1427,7 @@ defmodule Code do
Check `string_to_quoted/2` for options information.
"""
@doc since: "1.13.0"
@spec string_to_quoted_with_comments!(List.Chars.t(), keyword) :: {Macro.t(), list(map())}
@spec string_to_quoted_with_comments!(List.Chars.t(), parser_opts) :: {Macro.t(), list(map())}
def string_to_quoted_with_comments!(string, opts \\ []) do
charlist = to_charlist(string)
@@ -1456,6 +1512,9 @@ defmodule Code do
## Options
This function accepts all options supported by `format_string!/2` for controlling
code formatting, plus these additional options:
* `:comments` - the list of comments associated with the quoted expression.
Defaults to `[]`. It is recommended that both `:token_metadata` and
`:literal_encoder` options are given to `string_to_quoted_with_comments/2`
@@ -1466,17 +1525,13 @@ defmodule Code do
`string_to_quoted/2`, setting this option to `false` will prevent it from
escaping the sequences twice. Defaults to `true`.
* `:locals_without_parens` - a keyword list of name and arity
pairs that should be kept without parens whenever possible.
The arity may be the atom `:*`, which implies all arities of
that name. The formatter already includes a list of functions
and this option augments this list.
* `:syntax_colors` - a keyword list of colors the output is colorized.
See `Inspect.Opts` for more information.
See `format_string!/2` for the full list of formatting options including
`:file`, `:line`, `:line_length`, `:locals_without_parens`, `:force_do_end_blocks`,
`:syntax_colors`, and all migration options like `:migrate_charlists_as_sigils`.
"""
@doc since: "1.13.0"
@spec quoted_to_algebra(Macro.t(), keyword) :: Inspect.Algebra.t()
@spec quoted_to_algebra(Macro.t(), [format_opt() | quoted_to_algebra_opt()]) ::
Inspect.Algebra.t()
def quoted_to_algebra(quoted, opts \\ []) do
quoted
|> Code.Normalizer.normalize(opts)
+1
View File
@@ -158,6 +158,7 @@ defmodule Code.Formatter do
@doc """
Converts the quoted expression into an algebra document.
"""
@spec to_algebra(Macro.t(), keyword()) :: Inspect.Algebra.t()
def to_algebra(quoted, opts \\ []) do
comments = Keyword.get(opts, :comments, [])
+35 -8
View File
@@ -11,6 +11,26 @@ defmodule Code.Fragment do
@type position :: {line :: pos_integer(), column :: pos_integer()}
@typedoc """
Options for cursor context functions.
Currently, these options are not used but reserved for future extensibility.
"""
@type cursor_opts :: []
@typedoc """
Options for converting code fragments to quoted expressions.
"""
@type container_cursor_to_quoted_opts :: [
file: String.t(),
line: pos_integer(),
column: pos_integer(),
columns: boolean(),
token_metadata: boolean(),
literal_encoder: (term(), Macro.metadata() -> term()),
trailing_fragment: String.t()
]
@doc ~S"""
Returns the list of lines in the given string, preserving their line endings.
@@ -172,7 +192,7 @@ defmodule Code.Fragment do
references, and more.
"""
@doc since: "1.13.0"
@spec cursor_context(List.Chars.t(), keyword()) ::
@spec cursor_context(List.Chars.t(), cursor_opts()) ::
{:alias, charlist}
| {:alias, inside_alias, charlist}
| {:block_keyword_or_binary_operator, charlist}
@@ -282,7 +302,8 @@ defmodule Code.Fragment do
{{:local_or_var, acc}, count} -> {{:local_arity, acc}, count}
{{:dot, base, acc}, count} -> {{:dot_arity, base, acc}, count}
{{:operator, acc}, count} -> {{:operator_arity, acc}, count}
{_, _} -> {:none, 0}
{{:sigil, _}, _} -> {:none, 0}
{_, _} -> {{:operator, ~c"/"}, 1}
end
end
@@ -315,7 +336,7 @@ defmodule Code.Fragment do
end
defp identifier_to_cursor_context([?., ?., ?: | _], n, _), do: {{:unquoted_atom, ~c".."}, n + 3}
defp identifier_to_cursor_context([?., ?., ?. | _], n, _), do: {{:local_or_var, ~c"..."}, n + 3}
defp identifier_to_cursor_context([?., ?., ?. | _], n, _), do: {{:operator, ~c"..."}, n + 3}
defp identifier_to_cursor_context([?., ?: | _], n, _), do: {{:unquoted_atom, ~c"."}, n + 2}
defp identifier_to_cursor_context([?., ?. | _], n, _), do: {{:operator, ~c".."}, n + 2}
@@ -662,7 +683,7 @@ defmodule Code.Fragment do
of examples and their return values.
"""
@doc since: "1.13.0"
@spec surround_context(List.Chars.t(), position(), keyword()) ::
@spec surround_context(List.Chars.t(), position(), cursor_opts()) ::
%{begin: position, end: position, context: context} | :none
when context:
{:alias, charlist}
@@ -771,6 +792,12 @@ defmodule Code.Fragment do
{{:local_or_var, acc}, offset} ->
build_surround({:local_or_var, acc}, reversed, line, offset)
{{:block_keyword_or_binary_operator, acc}, offset} when acc in @textual_operators ->
build_surround({:operator, acc}, reversed, line, offset)
{{:block_keyword_or_binary_operator, acc}, offset} when acc in @keywords ->
build_surround({:keyword, acc}, reversed, line, offset)
{{:module_attribute, ~c""}, offset} ->
build_surround({:operator, ~c"@"}, reversed, line, offset)
@@ -1187,10 +1214,10 @@ defmodule Code.Fragment do
Defaults to `"nofile"`.
* `:line` - the starting line of the string being parsed.
Defaults to 1.
Defaults to `1`.
* `:column` - the starting column of the string being parsed.
Defaults to 1.
Defaults to `1`.
* `:columns` - when `true`, attach a `:column` key to the quoted
metadata. Defaults to `false`.
@@ -1209,7 +1236,7 @@ defmodule Code.Fragment do
"""
@doc since: "1.13.0"
@spec container_cursor_to_quoted(List.Chars.t(), keyword()) ::
@spec container_cursor_to_quoted(List.Chars.t(), container_cursor_to_quoted_opts()) ::
{:ok, Macro.t()} | {:error, {location :: keyword, binary | {binary, binary}, binary}}
def container_cursor_to_quoted(fragment, opts \\ []) do
{trailing_fragment, opts} = Keyword.pop(opts, :trailing_fragment)
@@ -1305,7 +1332,7 @@ defmodule Code.Fragment do
defp drop_tokens([{:do, _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([_ | tokens], counter), do: drop_tokens(tokens, counter)
defp drop_tokens([], 0), do: []
defp drop_tokens([], _counter), do: []
defp maybe_missing_stab?([{:after, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:do, _} | _], _stab_choice?), do: true
+1
View File
@@ -14,6 +14,7 @@ defmodule Code.Normalizer do
Wraps literals in the quoted expression to conform to the AST format expected
by the formatter.
"""
@spec normalize(Macro.t(), keyword()) :: Macro.t()
def normalize(quoted, opts \\ []) do
line = Keyword.get(opts, :line, nil)
escape = Keyword.get(opts, :escape, true)
+7 -1
View File
@@ -98,6 +98,12 @@ defmodule Config do
(assembled with `mix release`).
"""
@type config_opts :: [
imports: [Path.t()] | :disabled,
env: atom(),
target: atom()
]
@opts_key {__MODULE__, :opts}
@config_key {__MODULE__, :config}
@imports_key {__MODULE__, :imports}
@@ -306,7 +312,7 @@ defmodule Config do
end
@doc false
@spec __eval__!(Path.t(), binary(), keyword) :: {keyword, [Path.t()] | :disabled}
@spec __eval__!(Path.t(), binary(), config_opts) :: {keyword, [Path.t()] | :disabled}
def __eval__!(file, content, opts \\ []) when is_binary(file) and is_list(opts) do
env = Keyword.get(opts, :env)
target = Keyword.get(opts, :target)
+11
View File
@@ -111,6 +111,16 @@ defmodule Config.Provider do
"""
@type config_path :: {:system, binary(), binary()} | binary()
@typedoc """
Options for `init/3`.
"""
@type init_opts :: [
extra_config: config(),
prune_runtime_sys_config_after_boot: boolean(),
reboot_system_after_config: boolean(),
validate_compile_env: [{atom(), [atom()], term()}]
]
@doc """
Invoked when initializing a config provider.
@@ -196,6 +206,7 @@ defmodule Config.Provider do
@reboot_mode_key :config_provider_reboot_mode
@doc false
@spec init([{module(), term()}], config_path(), init_opts()) :: config()
def init(providers, config_path, opts \\ []) when is_list(providers) and is_list(opts) do
validate_config_path!(config_path)
providers = for {provider, init} <- providers, do: {provider, provider.init(init)}
+9 -3
View File
@@ -46,6 +46,12 @@ defmodule Config.Reader do
@behaviour Config.Provider
@type config_opts :: [
imports: [Path.t()] | :disabled,
env: atom(),
target: atom()
]
@impl true
def init(opts) when is_list(opts) do
{path, opts} = Keyword.pop!(opts, :path)
@@ -68,7 +74,7 @@ defmodule Config.Reader do
Accepts the same options as `read!/2`.
"""
@doc since: "1.11.0"
@spec eval!(Path.t(), binary, keyword) :: keyword
@spec eval!(Path.t(), binary, config_opts) :: keyword
def eval!(file, contents, opts \\ [])
when is_binary(file) and is_binary(contents) and is_list(opts) do
Config.__eval__!(Path.expand(file), contents, opts) |> elem(0)
@@ -90,7 +96,7 @@ defmodule Config.Reader do
"""
@doc since: "1.9.0"
@spec read!(Path.t(), keyword) :: keyword
@spec read!(Path.t(), config_opts) :: keyword
def read!(file, opts \\ []) when is_binary(file) and is_list(opts) do
file = Path.expand(file)
Config.__eval__!(file, File.read!(file), opts) |> elem(0)
@@ -104,7 +110,7 @@ defmodule Config.Reader do
option cannot be disabled in `read_imports!/2`.
"""
@doc since: "1.9.0"
@spec read_imports!(Path.t(), keyword) :: {keyword, [Path.t()]}
@spec read_imports!(Path.t(), config_opts) :: {keyword, [Path.t()]}
def read_imports!(file, opts \\ []) when is_binary(file) and is_list(opts) do
if opts[:imports] == :disabled do
raise ArgumentError, ":imports must be a list of paths"
+16 -64
View File
@@ -137,67 +137,6 @@ defmodule DynamicSupervisor do
A supervisor is bound to the same name registration rules as a `GenServer`.
Read more about these rules in the documentation for `GenServer`.
## Migrating from Supervisor's :simple_one_for_one
In case you were using the deprecated `:simple_one_for_one` strategy from
the `Supervisor` module, you can migrate to the `DynamicSupervisor` in
few steps.
Imagine the given "old" code:
defmodule MySupervisor do
use Supervisor
def start_link(init_arg) do
Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
end
def start_child(foo, bar, baz) do
# This will start child by calling MyWorker.start_link(init_arg, foo, bar, baz)
Supervisor.start_child(__MODULE__, [foo, bar, baz])
end
@impl true
def init(init_arg) do
children = [
# Or the deprecated: worker(MyWorker, [init_arg])
%{id: MyWorker, start: {MyWorker, :start_link, [init_arg]}}
]
Supervisor.init(children, strategy: :simple_one_for_one)
end
end
It can be upgraded to the DynamicSupervisor like this:
defmodule MySupervisor do
use DynamicSupervisor
def start_link(init_arg) do
DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
end
def start_child(foo, bar, baz) do
# If MyWorker is not using the new child specs, we need to pass a map:
# spec = %{id: MyWorker, start: {MyWorker, :start_link, [foo, bar, baz]}}
spec = {MyWorker, foo: foo, bar: bar, baz: baz}
DynamicSupervisor.start_child(__MODULE__, spec)
end
@impl true
def init(init_arg) do
DynamicSupervisor.init(
strategy: :one_for_one,
extra_arguments: [init_arg]
)
end
end
The difference is that the `DynamicSupervisor` expects the child specification
at the moment `start_child/2` is called, and no longer on the init callback.
If there are any initial arguments given on initialization, such as `[initial_arg]`,
it can be given in the `:extra_arguments` flag on `DynamicSupervisor.init/1`.
"""
@behaviour GenServer
@@ -233,9 +172,10 @@ defmodule DynamicSupervisor do
@typedoc """
Return values of `start_child` functions.
Unlike `Supervisor`, this module ignores the child spec ids, so
`{:error, {:already_started, pid}}` is not returned for child specs given with the same id.
`{:error, {:already_started, pid}}` is returned however if a duplicate name is used when using
Unlike `Supervisor`, this module ignores the child spec ids,
so `{:error, {:already_started, pid}}` is not returned for child specs
given with the same id. `{:error, {:already_started, pid}}` is returned
however if a duplicate name is used when using
[name registration](`m:GenServer#module-name-registration`).
"""
@type on_start_child ::
@@ -415,6 +355,10 @@ defmodule DynamicSupervisor do
`{:error, {:already_started, pid}}` is returned however if a duplicate name is
used when using [name registration](`m:GenServer#module-name-registration`).
This function will block the `DynamicSupervisor` until the child initializes.
When starting too many processes dynamically, you may want to use a
`PartitionSupervisor` to split the work across multiple processes.
If the child process start function returns `{:ok, child}` or `{:ok, child,
info}`, then child specification and PID are added to the supervisor and
this function returns the same value.
@@ -518,6 +462,14 @@ defmodule DynamicSupervisor do
@doc """
Terminates the given child identified by `pid`.
This function will block the `DynamicSupervisor` until the child
terminates, which may take an arbitrary amount of time if the child
is trapping exits and implements its own terminate callback.
For this reason, it is often better to ask the child process
itself to terminate, often by declaring in its child spec it has
a restart strategy of `:transient` (or `:temporary`) and then
sending it a message to stop with reason `:shutdown`.
If successful, this function returns `:ok`. If there is no process with
the given PID, this function returns `{:error, :not_found}`.
"""
+13 -5
View File
@@ -3611,9 +3611,14 @@ defmodule Enum do
end
def take(enumerable, amount) when is_integer(amount) and amount < 0 do
{count, fun} = slice_count_and_fun(enumerable, 1)
first = Kernel.max(amount + count, 0)
fun.(first, count - first, 1)
case slice_count_and_fun(enumerable, 1) do
{0, _fun} ->
[]
{count, fun} ->
first = Kernel.max(amount + count, 0)
fun.(first, count - first, 1)
end
end
@doc """
@@ -5118,6 +5123,9 @@ defimpl Enumerable, for: Range do
slice(Map.put(range, :step, step))
end
defp slice(_current, _step, 0), do: []
defp slice(current, step, remaining), do: [current | slice(current + step, step, remaining - 1)]
defp slice(current, _step, 1), do: [current]
defp slice(current, step, remaining) when remaining > 1 do
[current | slice(current + step, step, remaining - 1)]
end
end
+5 -6
View File
@@ -188,13 +188,12 @@ defmodule Exception do
term
|> inspect(pretty: true)
|> String.split("\n")
|> Enum.map(fn
|> Enum.map_intersperse("\n", fn
"" -> ""
line -> " " <> line
end)
|> Enum.join("\n")
message <> "\n\n" <> inspected
IO.iodata_to_binary([message, "\n\n", inspected, "\n"])
end
@doc """
@@ -1912,7 +1911,7 @@ defmodule UndefinedFunctionError do
end
defp format_fa({_dist, fun, arity}) do
[" * ", Macro.inspect_atom(:remote_call, fun), ?/, Integer.to_string(arity), ?\n]
[" * ", Macro.inspect_atom(:remote_call, fun), ?/, Integer.to_string(arity), ?\n]
end
defp exports_for(module) do
@@ -2244,7 +2243,7 @@ defmodule KeyError do
case suggestions do
[] -> []
suggestions -> ["\n\nDid you mean:\n\n" | format_suggestions(suggestions)]
suggestions -> ["\nDid you mean:\n\n" | format_suggestions(suggestions)]
end
end
@@ -2253,7 +2252,7 @@ defmodule KeyError do
|> Enum.sort(&(elem(&1, 0) >= elem(&2, 0)))
|> Enum.take(@max_suggestions)
|> Enum.sort(&(elem(&1, 1) <= elem(&2, 1)))
|> Enum.map(fn {_, key} -> [" * ", inspect(key), ?\n] end)
|> Enum.map(fn {_, key} -> [" * ", inspect(key), ?\n] end)
end
end
+1 -1
View File
@@ -317,7 +317,7 @@ defmodule File do
directories of `path`
* `:enospc` - there is no space left on the device
* `:enotdir` - a component of `path` is not a directory
* `:eperm` - missed required permisions
* `:eperm` - missed required permissions
## Examples
+5 -27
View File
@@ -662,36 +662,14 @@ end
defimpl Inspect, for: Any do
def inspect(%module{} = struct, opts) do
try do
module.__info__(:struct)
rescue
_ -> Inspect.Map.inspect_as_map(struct, opts)
else
info ->
if valid_struct?(info, struct) do
info =
for %{field: field} = map <- info,
field != :__exception__,
do: map
info =
for %{field: field} = map <- module.__info__(:struct),
field != :__exception__,
do: map
Inspect.Map.inspect_as_struct(struct, Macro.inspect_atom(:literal, module), info, opts)
else
Inspect.Map.inspect_as_map(struct, opts)
end
end
Inspect.Map.inspect_as_struct(struct, Macro.inspect_atom(:literal, module), info, opts)
end
defp valid_struct?(info, struct), do: valid_struct?(info, struct, map_size(struct) - 1)
defp valid_struct?([%{field: field} | info], struct, count) when is_map_key(struct, field),
do: valid_struct?(info, struct, count - 1)
defp valid_struct?([], _struct, 0),
do: true
defp valid_struct?(_fields, _struct, _count),
do: false
def inspect_as_struct(map, name, infos, opts) do
open = color_doc("#" <> name <> "<", :map, opts)
sep = color_doc(",", :map, opts)
+57 -5
View File
@@ -46,7 +46,7 @@ defmodule Inspect.Opts do
* `:limit` - limits the number of items that are inspected for tuples,
bitstrings, maps, lists and any other collection of items, with the exception of
printable strings and printable charlists which use the `:printable_limit` option.
It accepts a positive integer or `:infinity`. It defaults to 100 since
It accepts a positive integer or `:infinity`. It defaults to `100` since
`Elixir v1.19.0`, as it has better defaults to deal with nested collections.
* `:pretty` - if set to `true` enables pretty printing. Defaults to `false`.
@@ -115,11 +115,28 @@ defmodule Inspect.Opts do
width: non_neg_integer | :infinity
}
@typedoc """
Options for building an `Inspect.Opts` struct with `new/1`.
"""
@type new_opt ::
{:base, :decimal | :binary | :hex | :octal}
| {:binaries, :infer | :as_binaries | :as_strings}
| {:charlists, :infer | :as_lists | :as_charlists}
| {:custom_options, keyword}
| {:inspect_fun, (any, t -> Inspect.Algebra.t())}
| {:limit, non_neg_integer | :infinity}
| {:pretty, boolean}
| {:printable_limit, non_neg_integer | :infinity}
| {:safe, boolean}
| {:structs, boolean}
| {:syntax_colors, [{color_key, IO.ANSI.ansidata()}]}
| {:width, non_neg_integer | :infinity}
@doc """
Builds an `Inspect.Opts` struct.
"""
@doc since: "1.13.0"
@spec new(keyword()) :: t
@spec new([new_opt()]) :: t
def new(opts) do
struct(%Inspect.Opts{inspect_fun: default_inspect_fun()}, opts)
end
@@ -324,6 +341,14 @@ defmodule Inspect.Algebra do
quote do: {:doc_color, unquote(doc), unquote(color)}
end
@typedoc """
Options for container documents.
"""
@type container_opts :: [
separator: String.t(),
break: :strict | :flex | :maybe
]
@docs [
:doc_break,
:doc_collapse,
@@ -371,7 +396,7 @@ defmodule Inspect.Algebra do
def to_doc_with_opts(term, opts)
def to_doc_with_opts(%_{} = struct, %Inspect.Opts{inspect_fun: fun} = opts) do
if opts.structs do
if opts.structs and valid_struct?(struct) do
try do
fun.(struct, opts)
rescue
@@ -428,6 +453,26 @@ defmodule Inspect.Algebra do
fun.(arg, opts) |> pack_opts(opts)
end
defp valid_struct?(%module{} = struct) do
try do
module.__info__(:struct)
rescue
_ -> false
else
info ->
valid_struct?(info, struct, map_size(struct) - 1)
end
end
defp valid_struct?([%{field: field} | info], struct, count) when is_map_key(struct, field),
do: valid_struct?(info, struct, count - 1)
defp valid_struct?([], _struct, 0),
do: true
defp valid_struct?(_fields, _struct, _count),
do: false
defp pack_opts({_doc, %Inspect.Opts{}} = doc_opts, _opts), do: doc_opts
defp pack_opts(doc, opts), do: {doc, opts}
@@ -440,7 +485,14 @@ defmodule Inspect.Algebra do
updated options from inspection.
"""
@doc since: "1.6.0"
@spec container_doc(t, [term], t, Inspect.Opts.t(), (term, Inspect.Opts.t() -> t), keyword()) ::
@spec container_doc(
t,
[term],
t,
Inspect.Opts.t(),
(term, Inspect.Opts.t() -> t),
container_opts()
) ::
t
def container_doc(left, collection, right, inspect_opts, fun, opts \\ []) do
container_doc_with_opts(left, collection, right, inspect_opts, fun, opts) |> elem(0)
@@ -496,7 +548,7 @@ defmodule Inspect.Algebra do
t,
Inspect.Opts.t(),
(term, Inspect.Opts.t() -> t),
keyword()
container_opts()
) ::
{t, Inspect.Opts.t()}
def container_doc_with_opts(left, collection, right, inspect_opts, fun, opts \\ [])
+36 -13
View File
@@ -128,6 +128,22 @@ defmodule IO do
@type nodata :: {:error, term} | :eof
@type chardata :: String.t() | maybe_improper_list(char | chardata, String.t() | [])
@type inspect_opts :: [Inspect.Opts.new_opt() | {:label, term}]
@typedoc """
Stacktrace information as keyword options for `warn/2`.
At least `:file` is required. Other options are optional and used
to provide more precise location information.
"""
@type warn_stacktrace_opts :: [
file: String.t(),
line: pos_integer(),
column: pos_integer(),
module: module(),
function: {atom(), arity()}
]
defguardp is_device(term) when is_atom(term) or is_pid(term)
defguardp is_iodata(data) when is_list(data) or is_binary(data)
@@ -346,7 +362,10 @@ defmodule IO do
#=> my_app.ex:4: MyApp.main/1
"""
@spec warn(chardata | String.Chars.t(), Exception.stacktrace() | keyword() | Macro.Env.t()) ::
@spec warn(
chardata | String.Chars.t(),
Exception.stacktrace() | warn_stacktrace_opts() | Macro.Env.t()
) ::
:ok
def warn(message, stacktrace_info)
@@ -448,13 +467,15 @@ defmodule IO do
## Examples
The following code:
IO.inspect(<<0, 1, 2>>, width: 40)
Prints:
<<0, 1, 2>>
We can use the `:label` option to decorate the output:
You can use the `:label` option to decorate the output:
IO.inspect(1..100, label: "a wonderful range")
@@ -462,21 +483,23 @@ defmodule IO do
a wonderful range: 1..100
The `:label` option is especially useful with pipelines:
Inspect truncates large inputs by default. The `:printable_limit` controls
the limit for strings and other string-like constructs (such as charlists):
[1, 2, 3]
|> IO.inspect(label: "before")
|> Enum.map(&(&1 * 2))
|> IO.inspect(label: "after")
|> Enum.sum()
"abc"
|> String.duplicate(9001)
|> IO.inspect(printable_limit: :infinity)
Prints:
For containers such as lists, maps, and tuples, the number of entries
is managed by the `:limit` option:
before: [1, 2, 3]
after: [2, 4, 6]
1..100
|> Enum.map(& {&1, &1})
|> Enum.into(%{})
|> IO.inspect(limit: :infinity)
"""
@spec inspect(item, keyword) :: item when item: var
@spec inspect(item, inspect_opts) :: item when item: var
def inspect(item, opts \\ []) do
inspect(:stdio, item, opts)
end
@@ -486,7 +509,7 @@ defmodule IO do
See `inspect/2` for a full list of options.
"""
@spec inspect(device, item, keyword) :: item when item: var
@spec inspect(device, item, inspect_opts) :: item when item: var
def inspect(device, item, opts) when is_device(device) and is_list(opts) do
label = if label = opts[:label], do: [to_chardata(label), ": "], else: []
opts = Inspect.Opts.new(opts)
+18 -4
View File
@@ -5,6 +5,20 @@
defmodule IO.ANSI.Docs do
@moduledoc false
@type print_opts :: [
enabled: boolean(),
doc_bold: [IO.ANSI.ansicode()],
doc_code: [IO.ANSI.ansicode()],
doc_headings: [IO.ANSI.ansicode()],
doc_metadata: [IO.ANSI.ansicode()],
doc_quote: [IO.ANSI.ansicode()],
doc_inline_code: [IO.ANSI.ansicode()],
doc_table_heading: [IO.ANSI.ansicode()],
doc_title: [IO.ANSI.ansicode()],
doc_underline: [IO.ANSI.ansicode()],
width: pos_integer()
]
@bullet_text_unicode "• "
@bullet_text_ascii "* "
@bullets [?*, ?-, ?+]
@@ -30,7 +44,7 @@ defmodule IO.ANSI.Docs do
Values for the color settings are strings with
comma-separated ANSI values.
"""
@spec default_options() :: keyword
@spec default_options() :: print_opts
def default_options do
[
enabled: true,
@@ -52,7 +66,7 @@ defmodule IO.ANSI.Docs do
See `default_options/0` for docs on the supported options.
"""
@spec print_headings([String.t()], keyword) :: :ok
@spec print_headings([String.t()], print_opts) :: :ok
def print_headings(headings, options \\ []) do
# It's possible for some of the headings to contain newline characters (`\n`), so in order to prevent it from
# breaking the output from `print_headings/2`, as `print_headings/2` tries to pad the whole heading, we first split
@@ -77,7 +91,7 @@ defmodule IO.ANSI.Docs do
See `default_options/0` for docs on the supported options.
"""
@spec print_metadata(map, keyword) :: :ok
@spec print_metadata(map, print_opts) :: :ok
def print_metadata(metadata, options \\ []) when is_map(metadata) do
options = Keyword.merge(default_options(), options)
print_each_metadata(metadata, options) && IO.write("\n")
@@ -115,7 +129,7 @@ defmodule IO.ANSI.Docs do
It takes a set of `options` defined in `default_options/0`.
"""
@spec print(term(), String.t(), keyword) :: :ok
@spec print(term(), String.t(), print_opts) :: :ok
def print(doc, format, options \\ [])
def print(doc, "text/markdown", options) when is_binary(doc) and is_list(options) do
+17 -1
View File
@@ -328,6 +328,22 @@ defmodule JSON do
| {:invalid_byte, non_neg_integer(), byte()}
| {:unexpected_sequence, non_neg_integer(), binary()}
@typedoc """
Decoders for customizing JSON decoding behavior.
"""
@type decoders :: [
array_start: (term() -> term()),
array_push: (term(), term() -> term()),
array_finish: (term(), term() -> {term(), term()}),
object_start: (term() -> term()),
object_push: (term(), term(), term() -> term()),
object_finish: (term(), term() -> {term(), term()}),
float: (String.t() -> term()),
integer: (String.t() -> term()),
string: (String.t() -> term()),
null: term()
]
@doc ~S"""
Decodes the given JSON.
@@ -381,7 +397,7 @@ defmodule JSON do
For streaming decoding, see Erlang's [`:json`](`:json`) module.
"""
@spec decode(binary(), term(), keyword()) ::
@spec decode(binary(), term(), decoders()) ::
{term(), term(), binary()} | {:error, decode_error_reason()}
def decode(binary, acc, decoders) when is_binary(binary) and is_list(decoders) do
decoders = Keyword.put_new(decoders, :null, nil)
+13 -23
View File
@@ -231,7 +231,7 @@ defmodule Kernel do
Finally, note there is an overall structural sorting order, called
"Term Ordering", defined below. This order is provided for reference
purposes, it is not required by Elixir developers to know it by heart.
purposes, it is not required for Elixir developers to know it by heart.
### Term ordering
@@ -2456,7 +2456,7 @@ defmodule Kernel do
See the "Deriving" section of the documentation of the `Inspect`
protocol for more information.
"""
@spec inspect(Inspect.t(), keyword) :: String.t()
@spec inspect(Inspect.t(), [Inspect.Opts.new_opt()]) :: String.t()
def inspect(term, opts \\ []) when is_list(opts) do
opts = Inspect.Opts.new(opts)
@@ -2815,7 +2815,7 @@ defmodule Kernel do
This is most commonly used in pipelines, using the `|>/2` operator, allowing you
to pipe a value to a function outside of its first argument.
### Examples
## Examples
iex> 1 |> then(fn x -> x * 2 end)
2
@@ -3814,19 +3814,6 @@ defmodule Kernel do
{_, doc} when doc_attr? ->
do_at_escape(name, doc)
%{__struct__: Regex, source: source, opts: opts} = regex ->
# TODO: Remove this in Elixir v2.0
IO.warn(
"storing and reading regexes from module attributes is deprecated, " <>
"inline the regex inside the function definition instead",
env
)
case :erlang.system_info(:otp_release) < [?2, ?8] do
true -> do_at_escape(name, regex)
false -> quote(do: Regex.compile!(unquote(source), unquote(opts)))
end
value ->
do_at_escape(name, value)
end
@@ -3872,7 +3859,9 @@ defmodule Kernel do
defp do_at_escape(name, value) do
try do
:elixir_quote.escape(value, :none, false)
# mark module attrs as shallow-generated since the ast for their representation
# might contain opaque terms
Macro.escape(value, generated: true)
rescue
ex in [ArgumentError] ->
raise ArgumentError,
@@ -5197,7 +5186,7 @@ defmodule Kernel do
quote(do: Kernel.LexicalTracker.read_cache(unquote(pid), unquote(integer)))
%{} ->
:elixir_quote.escape(block, :none, false)
:elixir_quote.escape(block, :escape, false)
end
versioned_vars = env.versioned_vars
@@ -5477,7 +5466,7 @@ defmodule Kernel do
store =
case unquoted_expr or unquoted_call do
true ->
:elixir_quote.escape({call, expr}, :none, true)
:elixir_quote.escape({call, expr}, :escape, true)
false ->
key = :erlang.unique_integer()
@@ -6653,13 +6642,14 @@ defmodule Kernel do
end
defp compile_regex(binary_or_tuple, options) do
# TODO: Remove this when we require Erlang/OTP 28+
case is_binary(binary_or_tuple) and :erlang.system_info(:otp_release) < [?2, ?8] do
bin_opts = :binary.list_to_bin(options)
case is_binary(binary_or_tuple) do
true ->
Macro.escape(Regex.compile!(binary_or_tuple, :binary.list_to_bin(options)))
Macro.escape(Regex.compile!(binary_or_tuple, bin_opts))
false ->
quote(do: Regex.compile!(unquote(binary_or_tuple), unquote(:binary.list_to_bin(options))))
quote(do: Regex.compile!(unquote(binary_or_tuple), unquote(bin_opts)))
end
end
+53 -15
View File
@@ -16,6 +16,38 @@ defmodule Kernel.ParallelCompiler do
@type warning() :: {file :: Path.t(), Code.position(), message :: String.t()}
@type error() :: {file :: Path.t(), Code.position(), message :: String.t()}
@typedoc """
Options for parallel compilation functions.
"""
@type compile_opts :: [
after_compile: (-> term()),
each_file: (Path.t() -> term()),
each_long_compilation: (Path.t() -> term()) | (Path.t(), pid() -> term()),
each_long_verification: (module() -> term()) | (module(), pid() -> term()),
each_module: (Path.t(), module(), binary() -> term()),
each_cycle: ([module()], [Code.diagnostic(:warning)] ->
{:compile, [module()], [Code.diagnostic(:warning)]}
| {:runtime, [module()], [Code.diagnostic(:warning)]}),
long_compilation_threshold: pos_integer(),
long_verification_threshold: pos_integer(),
verification: boolean(),
profile: :time,
dest: Path.t(),
beam_timestamp: term(),
return_diagnostics: boolean(),
max_concurrency: pos_integer()
]
@typedoc """
Options for requiring files in parallel.
"""
@type require_opts :: [
each_file: (Path.t() -> term()),
each_module: (Path.t(), module(), binary() -> term()),
max_concurrency: pos_integer(),
return_diagnostics: boolean()
]
@doc """
Starts a task for parallel compilation.
"""
@@ -114,10 +146,9 @@ defmodule Kernel.ParallelCompiler do
the current file stops being compiled until the dependency is
resolved.
It returns `{:ok, modules, warnings}` or `{:error, errors, warnings}`
by default but we recommend using `return_diagnostics: true` so it returns
diagnostics as maps as well as a map of compilation information.
The map has the shape of:
It must be invoked with `return_diagnostics: true` as option, so it returns
`{:ok, modules, warnings_info}` or `{:error, errors, warnings_info}`,
where `warnings_info` has the shape:
%{
runtime_warnings: [warning],
@@ -177,15 +208,16 @@ defmodule Kernel.ParallelCompiler do
* `:beam_timestamp` - the modification timestamp to give all BEAM files
* `:return_diagnostics` (since v1.15.0) - returns maps with information instead of
a list of warnings and returns diagnostics as maps instead of tuples
a list of warnings and returns diagnostics as maps instead of tuples.
This option must be set to true, except for backwards compatibibility reasons.
* `:max_concurrency` - the maximum number of files to compile in parallel.
Setting this option to 1 will compile files sequentially.
Defaults to the number of schedulers online, or at least 2.
Defaults to the number of schedulers online, or at least `2`.
"""
@doc since: "1.6.0"
@spec compile([Path.t()], keyword()) ::
@spec compile([Path.t()], compile_opts()) ::
{:ok, [atom], [warning] | info()}
| {:error, [error] | [Code.diagnostic(:error)], [warning] | info()}
def compile(files, options \\ []) when is_list(options) do
@@ -198,7 +230,7 @@ defmodule Kernel.ParallelCompiler do
See `compile/2` for more information.
"""
@doc since: "1.6.0"
@spec compile_to_path([Path.t()], Path.t(), keyword()) ::
@spec compile_to_path([Path.t()], Path.t(), compile_opts()) ::
{:ok, [atom], [warning] | info()}
| {:error, [error] | [Code.diagnostic(:error)], [warning] | info()}
def compile_to_path(files, path, options \\ []) when is_binary(path) and is_list(options) do
@@ -211,10 +243,9 @@ defmodule Kernel.ParallelCompiler do
Opposite to compile, dependencies are not attempted to be
automatically solved between files.
It returns `{:ok, modules, warnings}` or `{:error, errors, warnings}`
by default but we recommend using `return_diagnostics: true` so it returns
diagnostics as maps as well as a map of compilation information.
The map has the shape of:
It must be invoked with `return_diagnostics: true` as option, so it returns
`{:ok, modules, warnings_info}` or `{:error, errors, warnings_info}`,
where `warnings_info` has the shape:
%{
runtime_warnings: [warning],
@@ -231,11 +262,15 @@ defmodule Kernel.ParallelCompiler do
* `:max_concurrency` - the maximum number of files to compile in parallel.
Setting this option to 1 will compile files sequentially.
Defaults to the number of schedulers online, or at least 2.
Defaults to the number of schedulers online, or at least `2`.
* `:return_diagnostics` (since v1.15.0) - returns maps with information instead of
a list of warnings and returns diagnostics as maps instead of tuples.
This option must be set to true, except for backwards compatibibility reasons.
"""
@doc since: "1.6.0"
@spec require([Path.t()], keyword()) ::
@spec require([Path.t()], require_opts()) ::
{:ok, [atom], [warning] | info()}
| {:error, [error] | [Code.diagnostic(:error)], [warning] | info()}
def require(files, options \\ []) when is_list(options) do
@@ -286,7 +321,10 @@ defmodule Kernel.ParallelCompiler do
if Keyword.get(options, :return_diagnostics, false) do
{status, modules_or_errors, info}
else
IO.warn("you must pass return_diagnostics: true when invoking Kernel.ParallelCompiler")
IO.warn(
"you must pass return_diagnostics: true when invoking Kernel.ParallelCompiler functions"
)
to_tuples = &Enum.map(&1, fn diag -> {diag.file, diag.position, diag.message} end)
modules_or_errors =
+9 -1
View File
@@ -877,7 +877,15 @@ defmodule Kernel.Typespec do
defp typespec({:fun, meta, args}, vars, caller, state) do
{args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args)
{{:type, location(meta), :fun, args}, state}
if args != [] do
IO.warn(
"fun/#{length(args)} is not valid in typespecs. Either specify fun() or use (... -> return) instead",
caller
)
end
{{:type, location(meta), :fun, []}, state}
end
defp typespec({:..., _meta, _args}, _vars, caller, _state) do
+5 -4
View File
@@ -126,10 +126,11 @@ defmodule Kernel.Utils do
key == :__struct__ and raise(ArgumentError, "cannot set :__struct__ in struct definition")
try do
:elixir_quote.escape(val, :none, false)
:elixir_quote.escape(val, {:struct, module}, false)
rescue
e in [ArgumentError] ->
raise ArgumentError, "invalid value for struct field #{key}, " <> Exception.message(e)
raise ArgumentError,
"invalid default value for struct field #{key}, " <> Exception.message(e)
else
_ -> {key, val}
end
@@ -171,7 +172,7 @@ defmodule Kernel.Utils do
:lists.foreach(foreach, enforce_keys)
struct = :maps.from_list([__struct__: module] ++ fields)
escaped_struct = :elixir_quote.escape(struct, :none, false)
escaped_struct = :elixir_quote.escape(struct, {:struct, module}, false)
body =
case bootstrapped? do
@@ -217,7 +218,7 @@ defmodule Kernel.Utils do
case enforce_keys -- :maps.keys(struct) do
[] ->
mapper = fn {key, val} ->
%{field: key, default: val}
%{field: key, default: val, required: :lists.member(key, enforce_keys)}
end
:ets.insert(set, {{:elixir, :struct}, :lists.map(mapper, fields)})
+2 -2
View File
@@ -915,7 +915,7 @@ defmodule List do
If `prefix` is an empty list, it returns `true`.
### Examples
## Examples
iex> List.starts_with?([1, 2, 3], [1, 2])
true
@@ -945,7 +945,7 @@ defmodule List do
If `suffix` is an empty list, it returns `true`.
### Examples
## Examples
iex> List.ends_with?([1, 2, 3], [2, 3])
true
+123 -14
View File
@@ -197,6 +197,16 @@ defmodule Macro do
@typedoc "A captured remote function in the format of &Mod.fun/arity"
@type captured_remote_function :: fun
@type escape_opts :: [
unquote: boolean(),
prune_metadata: boolean(),
generated: boolean()
]
@type inspect_atom_opts :: [
escape: (binary(), char() -> binary())
]
@doc """
Breaks a pipeline expression into a list.
@@ -793,12 +803,18 @@ defmodule Macro do
* `:unquote` - when `true`, this function leaves `unquote/1` and
`unquote_splicing/1` expressions unescaped, effectively unquoting
the contents on escape. This option is useful only when escaping
ASTs which may have quoted fragments in them. Defaults to `false`.
ASTs which may have quoted fragments in them. Note this option
will give a special meaning to `quote`/`unquote` nodes, which need
to be valid AST before escaping. Defaults to `false`.
* `:prune_metadata` - when `true`, removes most metadata from escaped AST
nodes. Note this option changes the semantics of escaped code and
it should only be used when escaping ASTs. Defaults to `false`.
* `:generated` - (since v1.19.0) Whether the AST should be considered as generated
by the compiler or not. This means the compiler and tools like Dialyzer may not
emit certain warnings.
As an example for `:prune_metadata`, `ExUnit` stores the AST of every
assertion, so when an assertion fails we can show code snippets to users.
Without this option, each time the test module is compiled, we would get a
@@ -834,12 +850,82 @@ defmodule Macro do
`escape/2` is used to escape *values* (either directly passed or variable
bound), while `quote/2` produces syntax trees for
expressions.
## Dealing with references and other runtime values
Macros work at compile-time and therefore `Macro.escape/1` can only escape values
that are valid during compilation, such as numbers, atoms, tuples, maps, binaries,
etc.
However, you may have values at compile-time which cannot be escaped, such as
`reference`s and `pid`s, since the process or memory address they point to will
no longer exist once compilation completes. Attempting to escape said values will
raise an exception. This is a common issue when working with NIFs.
Luckily, Elixir v1.19 introduces a mechanism that allows those values to be escaped,
as long as they are encapsulated by a struct within a module that defines the
`__escape__/1` function. This is possible as long as the reference has a natural
text or binary representation that can be serialized during compilation.
Let's imagine we have the following struct:
defmodule WrapperStruct do
defstruct [:ref]
def new(...), do: %WrapperStruct{ref: ...}
# efficiently dump to / load from binaries
def dump_to_binary(%WrapperStruct{ref: ref}), do: ...
def load_from_binary(binary), do: %WrapperStruct{ref: ...}
end
Such a struct could not be used in module attributes or escaped with `Macro.escape/2`:
defmodule Foo do
@my_struct WrapperStruct.new(...)
def my_struct, do: @my_struct
end
** (ArgumentError) cannot inject attribute @my_struct into function/macro because cannot escape #Reference<...>
To address this, structs can re-define how they should be escaped by defining a custom
`__escape__/1` function which returns the AST. In our example:
defmodule WrapperStruct do
# ...
def __escape__(struct) do
# dump to a binary representation at compile-time
binary = dump_to_binary(struct)
quote do
# load from the binary representation at runtime
WrapperStruct.load_from_binary(unquote(Macro.escape(binary)))
end
end
end
Now, our example above will be expanded as:
def my_struct, do: WrapperStruct.load_from_binary(<<...>>)
When implementing `__escape__/1`, you must ensure that the quoted expression
will evaluate to a struct that represents the one given as argument.
"""
@spec escape(term, keyword) :: t()
@spec escape(term, escape_opts) :: t()
def escape(expr, opts \\ []) do
unquote = Keyword.get(opts, :unquote, false)
kind = if Keyword.get(opts, :prune_metadata, false), do: :prune_metadata, else: :none
:elixir_quote.escape(expr, kind, unquote)
kind = if Keyword.get(opts, :prune_metadata, false), do: :escape_and_prune, else: :escape
generated = Keyword.get(opts, :generated, false)
case :elixir_quote.escape(expr, kind, unquote) do
# mark module attrs as shallow-generated since the ast for their representation
# might contain opaque terms
{caller, meta, args} when generated and is_list(meta) ->
{caller, [generated: true] ++ meta, args}
ast ->
ast
end
end
# TODO: Deprecate me on Elixir v1.22
@@ -852,21 +938,39 @@ defmodule Macro do
end
@doc """
Extracts the struct information (equivalent to calling
`module.__info__(:struct)`).
Extracts the struct information.
This is useful when a struct needs to be expanded at
compilation time and the struct being expanded may or may
not have been compiled. This function is also capable of
expanding structs defined under the module being compiled.
not have been compiled (including structs in the defined
under the module being compiled). For compiled modules,
it will invoke `module.__info__(:struct)`.
Calling this function also adds an export dependency on the
given struct.
It will raise `ArgumentError` if the struct is not available.
## Compatibility considerations
This function currently returns both `:required` and `:default`
entries for each field. While this naming is inconsistent
(a required field should not have a default), this is done for
backwards compatibility purposes.
In future releases, Elixir may introduce truly required struct
fields, and therefore only one of required or default will be
present. Your code should prepare for such scenario accordingly.
"""
@doc since: "1.18.0"
@spec struct_info!(module(), Macro.Env.t()) ::
[%{field: atom(), required: boolean(), default: term()}]
[
%{
required(:field) => atom(),
optional(:required) => boolean(),
optional(:default) => term()
}
]
def struct_info!(module, env) when is_atom(module) do
case :elixir_map.maybe_load_struct_info([line: env.line], module, [], true, env) do
{:ok, info} -> info
@@ -1738,12 +1842,17 @@ defmodule Macro do
@doc """
Applies a `mod`, `function`, and `args` at compile-time in `caller`.
This is used when you want to programmatically invoke a macro at
compile-time.
This is used when you want to dynamically invoke a function at
compile-time and force it to be tracked as a compile-time dependency.
For example, this is used by `dbg/1` to force the `dbg_callback`
configuration to be a compile-time dependency.
If you want to "invoke" a macro instead, remember macros are by
definition compile-time, and you can use `Macro.expand/2`.
"""
@doc since: "1.16.0"
def compile_apply(mod, fun, args, caller) do
:elixir_env.trace({:remote_macro, [], mod, fun, length(args)}, caller)
:elixir_env.trace({:remote_function, [], mod, fun, length(args)}, %{caller | function: nil})
Kernel.apply(mod, fun, args)
end
@@ -2399,7 +2508,7 @@ defmodule Macro do
"""
@doc since: "1.14.0"
@spec inspect_atom(:literal | :key | :remote_call, atom, keyword) :: binary
@spec inspect_atom(:literal | :key | :remote_call, atom, inspect_atom_opts) :: binary
def inspect_atom(source_format, atom, opts \\ [])
def inspect_atom(:literal, atom, _opts) when is_nil(atom) or is_boolean(atom) do
@@ -2591,7 +2700,7 @@ defmodule Macro do
:ok
end
prelude = quote do: options = unquote(Macro.escape(options))
prelude = quote do: options = unquote(options)
acc = {prelude, dbg_format_header(env)}
{acc, nil} =
+62 -11
View File
@@ -70,6 +70,42 @@ defmodule Macro.Env do
@typep tracers :: [module]
@typep versioned_vars :: %{optional(variable) => var_version :: non_neg_integer}
@type define_import_opts :: [
trace: boolean(),
emit_warnings: boolean(),
info_callback: (atom() -> [{atom(), arity()}]),
only: :functions | :macros | [{atom(), arity()}],
except: [{atom(), arity()}],
warn: boolean()
]
@type define_alias_opts :: [
trace: boolean(),
as: atom(),
warn: boolean()
]
@type define_require_opts :: [
trace: boolean(),
as: atom(),
warn: boolean()
]
@type expand_alias_opts :: [
trace: boolean()
]
@type expand_import_opts :: [
allow_locals: boolean() | (-> function() | false),
check_deprecations: boolean(),
trace: boolean()
]
@type expand_require_opts :: [
check_deprecations: boolean(),
trace: boolean()
]
@type t :: %{
__struct__: __MODULE__,
aliases: aliases,
@@ -331,7 +367,7 @@ defmodule Macro.Env do
"""
@doc since: "1.17.0"
@spec define_require(t, Macro.metadata(), module) :: {:ok, t}
@spec define_require(t, Macro.metadata(), module, define_require_opts) :: {:ok, t}
def define_require(env, meta, module, opts \\ [])
when is_list(meta) and is_atom(module) and is_list(opts) do
{trace, opts} = Keyword.pop(opts, :trace, true)
@@ -391,7 +427,8 @@ defmodule Macro.Env do
"""
@doc since: "1.17.0"
@spec define_import(t, Macro.metadata(), module, keyword) :: {:ok, t} | {:error, String.t()}
@spec define_import(t, Macro.metadata(), module, define_import_opts) ::
{:ok, t} | {:error, String.t()}
def define_import(env, meta, module, opts \\ [])
when is_list(meta) and is_atom(module) and is_list(opts) do
{trace, opts} = Keyword.pop(opts, :trace, true)
@@ -441,7 +478,8 @@ defmodule Macro.Env do
"""
@doc since: "1.17.0"
@spec define_alias(t, Macro.metadata(), module, keyword) :: {:ok, t} | {:error, String.t()}
@spec define_alias(t, Macro.metadata(), module, define_alias_opts) ::
{:ok, t} | {:error, String.t()}
def define_alias(env, meta, module, opts \\ [])
when is_list(meta) and is_atom(module) and is_list(opts) do
{trace, opts} = Keyword.pop(opts, :trace, true)
@@ -487,7 +525,7 @@ defmodule Macro.Env do
"""
@doc since: "1.17.0"
@spec expand_alias(t, keyword, [atom()], keyword) ::
@spec expand_alias(t, keyword, [atom()], expand_alias_opts) ::
{:alias, atom()} | :error
def expand_alias(env, meta, list, opts \\ [])
when is_list(meta) and is_list(list) and is_list(opts) do
@@ -517,8 +555,15 @@ defmodule Macro.Env do
## Options
* `:allow_locals` - when set to `false`, it does not attempt to capture
local macros defined in the current module in `env`
* `:allow_locals` - controls how local macros are resolved.
Defaults to `true`.
- When `false`, does not attempt to capture local macros defined in the
current module in `env`
- When `true`, uses a default resolver that looks for public macros in
the current module
- When a function, it will be invoked to lazily compute a local function
(or return false). It has signature `(-> function() | false)`
* `:check_deprecations` - when set to `false`, does not check for deprecations
when expanding macros
@@ -527,7 +572,7 @@ defmodule Macro.Env do
"""
@doc since: "1.17.0"
@spec expand_import(t, keyword, atom(), arity(), keyword) ::
@spec expand_import(t, keyword, atom(), arity(), expand_import_opts) ::
{:macro, module(), (Macro.metadata(), args :: [Macro.t()] -> Macro.t())}
| {:function, module(), atom()}
| {:error, :not_found | {:conflict, module()} | {:ambiguous, [module()]}}
@@ -542,10 +587,16 @@ defmodule Macro.Env do
trace = Keyword.get(opts, :trace, true)
module = env.module
# When allow_locals is a callback, we don't need to pass module macros as extra
# because the callback will handle local macro resolution
extra =
case allow_locals and function_exported?(module, :__info__, 1) do
true -> [{module, module.__info__(:macros)}]
false -> []
if is_function(allow_locals, 0) do
[]
else
case allow_locals and function_exported?(module, :__info__, 1) do
true -> [{module, module.__info__(:macros)}]
false -> []
end
end
case :elixir_dispatch.expand_import(meta, name, arity, env, extra, allow_locals, trace) do
@@ -583,7 +634,7 @@ defmodule Macro.Env do
"""
@doc since: "1.17.0"
@spec expand_require(t, keyword, module(), atom(), arity(), keyword) ::
@spec expand_require(t, keyword, module(), atom(), arity(), expand_require_opts) ::
{:macro, module(), (Macro.metadata(), args :: [Macro.t()] -> Macro.t())}
| :error
def expand_require(env, meta, module, name, arity, opts \\ [])
+30 -8
View File
@@ -560,15 +560,20 @@ defmodule Module do
callback is invoked under different scenarios, Elixir provides no guarantees
of when in the compilation cycle nor in which process the callback runs.
Furthermore, after verification callbacks are not expected to raise.
Given they run after the code is compiled, artifacts have already been
written to disk, and therefore raising does not effectively halt compilation
and may leave unused artifacts on disk. If you must raise, use `@after_compile`
or other callback. Given modules have already been compiled, functions in
this module, such as `get_attribute/2`, which expect modules to not have been
yet compiled, do not work on `@after_verify` callback.
Accepts a module or a `{module, function_name}` tuple. The function
must take one argument: the module name. When just a module is provided,
the function is assumed to be `__after_verify__/1`.
Callbacks will run in the order they are registered.
`Module` functions expecting not yet compiled modules are no longer available
at the time `@after_verify` is invoked.
#### Example
defmodule MyModule do
@@ -681,12 +686,21 @@ defmodule Module do
This function is generated for all modules. It's similar to `module_info/1` but
includes some additional Elixir-specific information, such as struct and macro
information. For documentation, see `c:Module.__info__/1`.
'''
@type definition :: {atom, arity}
@type def_kind :: :def | :defp | :defmacro | :defmacrop
@type create_opts :: [
file: binary(),
line: pos_integer(),
generated: boolean()
]
@type get_definition_opts :: [
skip_clauses: boolean()
]
@extra_error_msg_defines? "Use Kernel.function_exported?/3 and Kernel.macro_exported?/3 " <>
"to check for public functions and macros instead"
@@ -711,7 +725,8 @@ defmodule Module do
* `:module` - the module atom name
* `:struct` - (since v1.14.0) if the module defines a struct and if so each field in order
* `:struct` - (since v1.14.0) if the module defines a struct and if so each field in order.
See `Macro.struct_info!/2` for more information
"""
@callback __info__(:attributes) :: keyword()
@@ -721,7 +736,14 @@ defmodule Module do
@callback __info__(:md5) :: binary()
@callback __info__(:module) :: module()
@callback __info__(:struct) ::
list(%{required(:field) => atom(), optional(:default) => term()}) | nil
[
%{
required(:field) => atom(),
optional(:required) => boolean(),
optional(:default) => term()
}
]
| nil
@doc """
Returns information about module attributes used by Elixir.
@@ -918,7 +940,7 @@ defmodule Module do
when defining the module, while `Kernel.defmodule/2`
automatically uses the environment it is invoked at.
"""
@spec create(module, Macro.t(), Macro.Env.t() | keyword) :: {:module, module, binary, term}
@spec create(module, Macro.t(), Macro.Env.t() | create_opts) :: {:module, module, binary, term}
def create(module, quoted, opts)
def create(module, quoted, %Macro.Env{} = env) when is_atom(module) do
@@ -1423,7 +1445,7 @@ defmodule Module do
only an interest in fetching the kind and the metadata
"""
@spec get_definition(module, definition, keyword) ::
@spec get_definition(module, definition, get_definition_opts) ::
{:v1, def_kind, meta :: keyword,
[{meta :: keyword, arguments :: [Macro.t()], guards :: [Macro.t()], Macro.t()}]}
| nil
+12 -1
View File
@@ -11,9 +11,20 @@ defmodule Module.ParallelChecker do
@type warning() :: term()
@type mode() :: :erlang | :elixir | :protocol
@typedoc """
Options for `start_link/1`.
"""
@type start_link_opts :: [
{:max_concurrency, pos_integer()}
| {:long_verification_threshold, pos_integer()}
| {:each_long_verification, (module() -> term()) | (module(), pid() -> term())}
| {atom(), term()}
]
@doc """
Initializes the parallel checker process.
"""
@spec start_link(start_link_opts()) :: {:ok, cache()}
def start_link(opts \\ []) do
:proc_lib.start_link(__MODULE__, :init, [opts])
end
@@ -412,7 +423,7 @@ defmodule Module.ParallelChecker do
mode =
with {^module, binary, _filename} <- object_code,
{:ok, {^module, [{~c"ExCk", chunk}]}} <- :beam_lib.chunks(binary, [~c"ExCk"]),
{:elixir_checker_v1, contents} <- :erlang.binary_to_term(chunk) do
{:elixir_checker_v3, contents} <- :erlang.binary_to_term(chunk) do
# The chunk has more information, so that's our preference
cache_chunk(table, module, contents)
else
+21 -9
View File
@@ -49,8 +49,8 @@ defmodule Module.Types do
finder =
fn fun_arity ->
case :lists.keyfind(fun_arity, 1, defs) do
{_, kind, _, _} = clause ->
{infer_mode(kind, infer_signatures?), clause, default_domain(fun_arity, impl)}
{_, kind, _, _} = def ->
default_domain(infer_mode(kind, infer_signatures?), def, fun_arity, impl)
false ->
false
@@ -82,7 +82,7 @@ defmodule Module.Types do
{types, context} ->
# Optimized version of finder, since we already the definition
finder = fn _ ->
{infer_mode(kind, infer_signatures?), def, default_domain(fun_arity, impl)}
default_domain(infer_mode(kind, infer_signatures?), def, fun_arity, impl)
end
{_kind, inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
@@ -146,12 +146,24 @@ defmodule Module.Types do
end
end
defp default_domain({_, arity} = fun_arity, impl) do
defp default_domain(mode, def, {_, arity} = fun_arity, impl) do
with {for, callbacks} <- impl,
true <- fun_arity in callbacks do
[Descr.dynamic(Module.Types.Of.impl(for)) | List.duplicate(Descr.dynamic(), arity - 1)]
args = [
Descr.dynamic(Module.Types.Of.impl(for))
| List.duplicate(Descr.dynamic(), arity - 1)
]
{fun_arity, kind, meta, clauses} = def
clauses =
for {meta, args, guards, body} <- clauses do
{[type_check: {:impl, for}] ++ meta, args, guards, body}
end
{mode, {fun_arity, kind, meta, clauses}, args}
else
_ -> List.duplicate(Descr.dynamic(), arity)
_ -> {mode, def, List.duplicate(Descr.dynamic(), arity)}
end
end
@@ -208,7 +220,7 @@ defmodule Module.Types do
finder = fn fun_arity ->
case :lists.keyfind(fun_arity, 1, defs) do
{_, _, _, _} = clause -> {:dynamic, clause, default_domain(fun_arity, impl)}
{_, _, _, _} = def -> default_domain(:dynamic, def, fun_arity, impl)
false -> false
end
end
@@ -219,7 +231,7 @@ defmodule Module.Types do
context =
Enum.reduce(defs, context(), fn {fun_arity, _kind, meta, _clauses} = def, context ->
# Optimized version of finder, since we already the definition
finder = fn _ -> {:dynamic, def, default_domain(fun_arity, impl)} end
finder = fn _ -> default_domain(:dynamic, def, fun_arity, impl) end
{_kind, _inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
context
end)
@@ -419,7 +431,7 @@ defmodule Module.Types do
local_handler: handler,
# Control if variable refinement is enabled.
# It is disabled only on dynamic dispatches.
refine_vars: true
refine_vars: false
}
end
+61 -20
View File
@@ -487,7 +487,7 @@ defmodule Module.Types.Apply do
{union(type, fun_from_non_overlapping_clauses(clauses)), fallback?, context}
{{:infer, _, clauses}, context} when length(clauses) <= @max_clauses ->
{union(type, fun_from_overlapping_clauses(clauses)), fallback?, context}
{union(type, fun_from_inferred_clauses(clauses)), fallback?, context}
{_, context} ->
{type, true, context}
@@ -705,7 +705,7 @@ defmodule Module.Types.Apply do
result =
case info do
{:infer, _, clauses} when length(clauses) <= @max_clauses ->
fun_from_overlapping_clauses(clauses)
fun_from_inferred_clauses(clauses)
_ ->
dynamic(fun(arity))
@@ -979,7 +979,7 @@ defmodule Module.Types.Apply do
{mod, fun, arity, converter} = mfac
meta = elem(expr, 1)
{banner, hints, traces} =
{banner, traces} =
case Keyword.get(meta, :type_check) do
:interpolation ->
{_, _, [arg]} = expr
@@ -990,7 +990,7 @@ defmodule Module.Types.Apply do
#{expr_to_string(arg) |> indent(4)}
it has type:
""", [:interpolation], collect_traces(expr, context)}
""", collect_traces(expr, context)}
:generator ->
{:<-, _, [_, arg]} = expr
@@ -1001,7 +1001,7 @@ defmodule Module.Types.Apply do
#{expr_to_string(expr) |> indent(4)}
it has type:
""", [:generator], collect_traces(arg, context)}
""", collect_traces(arg, context)}
:into ->
{"""
@@ -1010,7 +1010,7 @@ defmodule Module.Types.Apply do
into: #{expr_to_string(expr) |> indent(4)}
it has type:
""", [:into], collect_traces(expr, context)}
""", collect_traces(expr, context)}
_ ->
mfa_or_fa = if mod, do: Exception.format_mfa(mod, fun, arity), else: "#{fun}/#{arity}"
@@ -1021,27 +1021,68 @@ defmodule Module.Types.Apply do
#{expr_to_string(expr) |> indent(4)}
given types:
""", [], collect_traces(expr, context)}
""", collect_traces(expr, context)}
end
explanation =
{explanation, impls} =
cond do
reason = empty_arg_reason(converter.(args_types)) ->
reason
{reason, ""}
Code.ensure_loaded?(mod) and
Keyword.has_key?(mod.module_info(:attributes), :__protocol__) ->
# Protocol errors can be very verbose, so we collapse structs
"""
but expected a type that implements the #{inspect(mod)} protocol, it must be one of:
#{clauses_args_to_quoted_string(clauses, converter, collapse_structs: true)}
"""
if function_exported?(mod, :__protocol__, 1) and
mod.__protocol__(:impls) == {:consolidated, []} do
{"""
but the #{inspect(mod)} protocol was not yet implemented \
for any type and therefore will always fail.
This warning will disappear once you define a implementation. \
If the protocol is part of a library, you may define a dummy \
implementation for development/test.
""", ""}
else
fix =
case mod do
String.Chars ->
"""
You either passed the wrong value or you must:
1. convert the given value to a string explicitly
(use inspect/1 if you want to convert any data structure to a string)
2. implement the String.Chars protocol
"""
Enumerable ->
"""
You either passed the wrong value or you must:
1. convert the given value to an Enumerable explicitly
2. implement the Enumerable protocol
"""
_ ->
"""
You either passed the wrong value or you forgot to implement the protocol.
"""
end
{"""
but expected a type that implements the #{inspect(mod)} protocol.
#{fix}\
""",
"""
#{hint()} the #{inspect(mod)} protocol is implemented for the following types:
#{clauses_args_to_quoted_string(clauses, converter, collapse_structs: true)}
"""}
end
true ->
"""
but expected one of:
#{clauses_args_to_quoted_string(clauses, converter, [])}
"""
{"""
but expected one of:
#{clauses_args_to_quoted_string(clauses, converter, [])}
""", ""}
end
%{
@@ -1056,7 +1097,7 @@ defmodule Module.Types.Apply do
""",
explanation,
format_traces(traces),
format_hints(hints)
impls
])
}
end
@@ -1242,7 +1283,7 @@ defmodule Module.Types.Apply do
common = intersection(actual, expected)
uncommon_doc =
difference(actual, common)
difference(actual, expected)
|> to_quoted()
|> Code.Formatter.to_algebra()
|> ansi_red()
File diff suppressed because it is too large Load Diff
+78 -8
View File
@@ -231,21 +231,36 @@ defmodule Module.Types.Expr do
# %Struct{map | ...}
# This syntax is deprecated, so we simply traverse.
def of_expr(
{:%, _, [_, {:%{}, _, [{:|, _, [map, args]}]}]} = struct,
{:%, meta, [module, {:%{}, _, [{:|, _, [map, pairs]}]}]} = struct,
_expected,
expr,
stack,
context
) do
{_, context} = of_expr(map, term(), struct, stack, context)
{map_type, context} = of_expr(map, term(), struct, stack, context)
context =
Enum.reduce(args, context, fn {key, value}, context when is_atom(key) ->
{_, context} = of_expr(value, term(), expr, stack, context)
if stack.mode == :traversal do
context
end)
else
with {false, struct_key_type} <- map_fetch(map_type, :__struct__),
{:finite, [^module]} <- atom_fetch(struct_key_type) do
context
else
_ ->
error(__MODULE__, {:badupdate, map_type, struct, context}, meta, stack, context)
end
end
{dynamic(), context}
Enum.reduce(pairs, {map_type, context}, fn {key, value}, {acc, context} ->
# TODO: Once we support typed structs, we need to type check them here
{type, context} = of_expr(value, term(), expr, stack, context)
case map_fetch_and_put(acc, key, type) do
{_value, acc} -> {acc, context}
_ -> {acc, context}
end
end)
end
# %{...}
@@ -340,7 +355,7 @@ defmodule Module.Types.Expr do
add_inferred(acc, args, body)
end)
{fun_from_overlapping_clauses(acc), context}
{fun_from_inferred_clauses(acc), context}
end
end
@@ -461,7 +476,11 @@ defmodule Module.Types.Expr do
{args_types, context} =
Enum.map_reduce(args, context, &of_expr(&1, @pending, &1, stack, &2))
Apply.fun_apply(fun_type, args_types, call, stack, context)
if stack.mode == :traversal do
{dynamic(), context}
else
Apply.fun_apply(fun_type, args_types, call, stack, context)
end
end
def of_expr({{:., _, [callee, key_or_fun]}, meta, []} = call, expected, expr, stack, context)
@@ -791,6 +810,57 @@ defmodule Module.Types.Expr do
## Warning formatting
def format_diagnostic({:badupdate, type, expr, context}) do
{:%, _, [module, {:%{}, _, [{:|, _, [map, _]}]}]} = expr
traces = collect_traces(map, context)
fix =
case map do
{var, meta, context} when is_atom(var) and is_atom(context) ->
if capture = meta[:capture] do
"instead of using &#{capture}, you must define an anonymous function, define a variable and pattern match on \"%#{inspect(module)}{}\""
else
"when defining the variable \"#{Macro.to_string(map)}\", you must also pattern match on \"%#{inspect(module)}{}\""
end
_ ->
"you must assign \"#{Macro.to_string(map)}\" to variable and pattern match on \"%#{inspect(module)}{}\""
end
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
a struct for #{inspect(module)} is expected on struct update:
#{expr_to_string(expr, collapse_structs: false) |> indent(4)}
but got type:
#{to_quoted_string(type) |> indent(4)}
""",
format_traces(traces),
"""
#{fix}.
#{hint()} given pattern matching is enough to catch typing errors, \
you may optionally convert the struct update into a map update. For \
example, instead of:
user = some_function()
%User{user | name: "John Doe"}
it is enough to write:
%User{} = user = some_function()
%{user | name: "John Doe"}
"""
])
}
end
def format_diagnostic({:badmap, type, expr, context}) do
traces = collect_traces(expr, context)
+34 -24
View File
@@ -91,29 +91,6 @@ defmodule Module.Types.Helpers do
"var.fun()" (with parentheses) means "var" is an atom()
"""
:interpolation ->
"""
#{hint()} string interpolation uses the String.Chars protocol to \
convert a data structure into a string. Either convert the data type into a \
string upfront or implement the protocol accordingly
"""
:generator ->
"""
#{hint()} for-comprehensions use the Enumerable protocol to traverse \
data structures. Either convert the data type into a list (or another Enumerable) \
or implement the protocol accordingly
"""
:into ->
"""
#{hint()} the :into option in for-comprehensions use the Collectable protocol to \
build its result. Either pass a valid data type or implement the protocol accordingly
"""
:anonymous_rescue ->
"""
@@ -132,6 +109,20 @@ defmodule Module.Types.Helpers do
the union (which may be none)
"""
{:impl, for} ->
# Get the type without dynamic for better pretty printing
type =
for
|> Module.Types.Of.impl()
|> Module.Types.Descr.dynamic()
|> Map.fetch!(:dynamic)
|> Module.Types.Descr.to_quoted_string(collapse_structs: true)
"""
#{hint()} defimpl for #{inspect(for)} requires its callbacks to match exclusively on #{type}
"""
:empty_domain ->
"""
@@ -141,7 +132,8 @@ defmodule Module.Types.Helpers do
end)
end
defp hint, do: :elixir_errors.prefix(:hint)
@doc "The hint prefix"
def hint, do: :elixir_errors.prefix(:hint)
@doc """
Collect traces from variables in expression.
@@ -270,6 +262,10 @@ defmodule Module.Types.Helpers do
translating inlined Erlang calls back to Elixir.
We also undo some macro expressions done by the Kernel module.
## Options
* `:collapse_structs` - when false, show structs full representation
"""
def expr_to_string(expr, opts \\ []) do
string = prewalk_expr_to_string(expr, opts)
@@ -340,6 +336,13 @@ defmodule Module.Types.Helpers do
{{:., _, [mod, fun]}, meta, args} ->
erl_to_ex(mod, fun, args, meta)
{:fn, meta, [{:->, _, [_args, return]}]} = expr ->
if meta[:capture] do
{:&, meta, [return]}
else
expr
end
{:&, amp_meta, [{:/, slash_meta, [{{:., dot_meta, [mod, fun]}, call_meta, []}, arity]}]} ->
{mod, fun} =
case :elixir_rewrite.erl_to_ex(mod, fun, arity) do
@@ -385,6 +388,13 @@ defmodule Module.Types.Helpers do
case
end
{var, meta, context} = expr when is_atom(var) and is_atom(context) ->
if is_integer(meta[:capture]) do
{:&, meta, [meta[:capture]]}
else
expr
end
other ->
other
end)
+10 -4
View File
@@ -259,12 +259,12 @@ defmodule Module.Types.Pattern do
defp badpattern_error(expr, index, tag, stack, context) do
meta =
if meta = get_meta(expr) do
meta ++ Keyword.take(stack.meta, [:generated, :line])
meta ++ Keyword.take(stack.meta, [:generated, :line, :type_check])
else
stack.meta
end
error(__MODULE__, {:badpattern, expr, index, tag, context}, meta, stack, context)
error(__MODULE__, {:badpattern, meta, expr, index, tag, context}, meta, stack, context)
end
defp of_pattern_intersect(tree, expected, expr, index, tag, stack, context) do
@@ -802,13 +802,19 @@ defmodule Module.Types.Pattern do
#
# The match pattern ones have the whole expression instead
# of a single pattern.
def format_diagnostic({:badpattern, pattern_or_expr, index, tag, context}) do
def format_diagnostic({:badpattern, meta, pattern_or_expr, index, tag, context}) do
{to_trace, message} = badpattern(tag, pattern_or_expr, index)
traces = collect_traces(to_trace, context)
hints =
case Keyword.get(meta, :type_check) do
{:impl, _} = impl -> [impl]
_ -> []
end
%{
details: %{typing_traces: traces},
message: IO.iodata_to_binary([message, format_traces(traces)])
message: IO.iodata_to_binary([message, format_traces(traces), format_hints(hints)])
}
end
+81 -10
View File
@@ -129,6 +129,7 @@ defmodule OptionParser do
* `:integer` - parses the value as an integer
* `:float` - parses the value as a float
* `:string` - parses the value as a string
* `:regex` - parses the value as a regular expression with Unicode support
If a switch can't be parsed according to the given type, it is
returned in the invalid options list.
@@ -282,11 +283,11 @@ defmodule OptionParser do
iex> OptionParser.parse!(["--limit", "xyz"], strict: [limit: :integer])
** (OptionParser.ParseError) 1 error found!
--limit : Expected type integer, got "xyz"
--limit : Expected type integer, got "xyz"...
iex> OptionParser.parse!(["--unknown", "xyz"], strict: [])
** (OptionParser.ParseError) 1 error found!
--unknown : Unknown option
--unknown : Unknown option...
iex> OptionParser.parse!(
...> ["-l", "xyz", "-f", "bar"],
@@ -295,7 +296,7 @@ defmodule OptionParser do
...> )
** (OptionParser.ParseError) 2 errors found!
-l : Expected type integer, got "xyz"
-f : Expected type integer, got "bar"
-f : Expected type integer, got "bar"...
"""
@spec parse!(argv, options) :: {parsed, argv}
@@ -354,7 +355,7 @@ defmodule OptionParser do
...> strict: [number: :integer]
...> )
** (OptionParser.ParseError) 1 error found!
--number : Expected type integer, got "lib"
--number : Expected type integer, got "lib"...
iex> OptionParser.parse_head!(
...> ["--verbose", "--source", "lib", "test/enum_test.exs", "--unlock"],
@@ -362,7 +363,7 @@ defmodule OptionParser do
...> )
** (OptionParser.ParseError) 2 errors found!
--verbose : Missing argument of type integer
--source : Expected type integer, got "lib"
--source : Expected type integer, got "lib"...
"""
@spec parse_head!(argv, options) :: {parsed, argv}
@@ -664,7 +665,7 @@ defmodule OptionParser do
end
defp validate_switch({_name, type_or_type_and_modifiers}) do
valid = [:boolean, :count, :integer, :float, :string, :keep]
valid = [:boolean, :count, :integer, :float, :string, :regex, :keep]
invalid = List.wrap(type_or_type_and_modifiers) -- valid
if invalid != [] do
@@ -704,6 +705,12 @@ defmodule OptionParser do
_ -> {true, value}
end
:regex in kinds ->
case Regex.compile(value, "u") do
{:ok, regex} -> {false, regex}
{:error, _} -> {true, value}
end
true ->
{false, value}
end
@@ -863,15 +870,20 @@ defmodule OptionParser do
error_count = length(errors)
error = if error_count == 1, do: "error", else: "errors"
"#{error_count} #{error} found!\n" <>
Enum.map_join(errors, "\n", &format_error(&1, opts, types))
slogan =
"#{error_count} #{error} found!\n" <>
Enum.map_join(errors, "\n", &format_error(&1, opts, types))
case format_available_options(opts, types) do
"" -> slogan
available_options -> slogan <> "\n\n#{available_options}"
end
end
defp format_error({option, nil}, opts, types) do
if type = get_type(option, opts, types) do
if String.contains?(option, "_") do
msg = "#{option} : Unknown option"
msg <> ". Did you mean #{String.replace(option, "_", "-")}?"
else
"#{option} : Missing argument of type #{type}"
@@ -891,7 +903,13 @@ defmodule OptionParser do
defp format_error({option, value}, opts, types) do
type = get_type(option, opts, types)
"#{option} : Expected type #{type}, got #{inspect(value)}"
with :regex <- type,
{:error, {reason, position}} <- Regex.compile(value, "u") do
"#{option} : Invalid regular expression #{inspect(value)}: #{reason} at position #{position}"
else
_ -> "#{option} : Expected type #{type}, got #{inspect(value)}"
end
end
defp get_type(option, opts, types) do
@@ -917,4 +935,57 @@ defmodule OptionParser do
option = String.replace(source, "_", "-")
if score < current, do: best, else: {option, score}
end
defp format_available_options(opts, switches) do
reverse_aliases =
opts
|> Keyword.get(:aliases, [])
|> Enum.reduce(%{}, fn {alias, target}, acc ->
Map.update(acc, target, [alias], &[alias | &1])
end)
formatted_options =
switches
|> Enum.sort()
|> Enum.map(fn {name, types} ->
types = List.wrap(types)
case types |> List.delete(:keep) |> List.first(:string) do
:boolean ->
base = "#{to_switch(name)}, #{to_switch(name, "--no-")}"
add_aliases(base, name, reverse_aliases)
type ->
base = "#{to_switch(name)} #{String.upcase(Atom.to_string(type))}"
base = add_aliases(base, name, reverse_aliases)
if :keep in types do
base <> " (may be given more than once)"
else
base
end
end
end)
if formatted_options == [] do
""
else
"Supported options:\n" <> Enum.map_join(formatted_options, "\n", &(" " <> &1))
end
end
defp add_aliases(base, name, reverse_aliases) do
case Map.get(reverse_aliases, name, []) do
[] ->
base
alias_list ->
alias_str =
alias_list
|> Enum.sort()
|> Enum.map_join(", ", &("-" <> Atom.to_string(&1)))
base <> " (alias: #{alias_str})"
end
end
end
+5 -3
View File
@@ -22,6 +22,8 @@ defmodule Path do
"""
@type t :: IO.chardata()
@type relative_to_opts :: [force: boolean()]
@doc """
Converts the given path to an absolute one.
@@ -401,7 +403,7 @@ defmodule Path do
Path.relative_to("../foo", "/usr/local") #=> "../foo"
"""
@spec relative_to(t, t, keyword) :: binary
@spec relative_to(t, t, relative_to_opts) :: binary
def relative_to(path, cwd, opts \\ []) when is_list(opts) do
os_type = major_os_type()
split_path = split(path)
@@ -479,7 +481,7 @@ defmodule Path do
Check `relative_to/3` for the supported options.
"""
@spec relative_to_cwd(t, keyword) :: binary
@spec relative_to_cwd(t, relative_to_opts) :: binary
def relative_to_cwd(path, opts \\ []) when is_list(opts) do
case :file.get_cwd() do
{:ok, base} -> relative_to(path, IO.chardata_to_string(base), opts)
@@ -801,7 +803,7 @@ defmodule Path do
Path.wildcard("projects/*/ebin/**/*.{beam,app}")
"""
@spec wildcard(t, keyword) :: [binary]
@spec wildcard(t, match_dot: boolean()) :: [binary]
def wildcard(glob, opts \\ []) when is_list(opts) do
mod = if Keyword.get(opts, :match_dot), do: :file, else: Path.Wildcard
+1 -1
View File
@@ -78,7 +78,7 @@ defmodule Port do
The port can be opened through four main mechanisms.
As a short summary, prefer to using the `:spawn` and `:spawn_executable`
As a short summary, prefer to use the `:spawn` and `:spawn_executable`
options mentioned below. The other two options, `:spawn_driver` and `:fd`
are for advanced usage within the VM. Also consider using `System.cmd/3`
if all you want is to execute a program and retrieve its return value.
+2 -10
View File
@@ -669,7 +669,7 @@ defmodule Protocol do
{Descr.term(), clauses, clauses}
else
{domain, clauses ++ [{[not_domain], Descr.atom([nil])}], clauses}
{domain, [{[Descr.term()], Descr.atom()}], clauses}
end
end
@@ -770,15 +770,7 @@ defmodule Protocol do
# We don't allow function definition inside protocols
import Kernel,
except: [
def: 1,
def: 2,
defdelegate: 2,
defguard: 1,
defguardp: 1,
defstruct: 1,
defexception: 1
]
except: [def: 1, def: 2, defdelegate: 2, defguard: 1, defguardp: 1]
# Import the new `def` that is used by protocols
import Protocol, only: [def: 1]
+9 -2
View File
@@ -45,6 +45,13 @@ defmodule Record do
a module by calling `Code.fetch_docs/1`.
"""
@type extract_opts :: [
from: binary(),
from_lib: binary(),
includes: [binary()],
macros: keyword()
]
@doc """
Extracts record information from an Erlang file.
@@ -102,7 +109,7 @@ defmodule Record do
]
"""
@spec extract(name :: atom, keyword) :: keyword
@spec extract(name :: atom, extract_opts) :: keyword
def extract(name, opts) when is_atom(name) and is_list(opts) do
Record.Extractor.extract(name, opts)
end
@@ -119,7 +126,7 @@ defmodule Record do
Accepts the same options as listed for `Record.extract/2`.
"""
@spec extract_all(keyword) :: [{name :: atom, keyword}]
@spec extract_all(extract_opts) :: [{name :: atom, keyword}]
def extract_all(opts) when is_list(opts) do
Record.Extractor.extract_all(opts)
end
+98 -9
View File
@@ -3,6 +3,8 @@
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Regex do
# TODO: Remove the "Starting from Erlang/OTP 28" part in the Modifiers'
# section once Erlang/OTP 28+ is exclusively supported.
@moduledoc ~S"""
Provides regular expressions for Elixir.
@@ -76,9 +78,16 @@ defmodule Regex do
* `:caseless` (i) - adds case insensitivity
* `:dotall` (s) - causes dot to match newlines and also set newline to
anycrlf; the new line setting can be overridden by setting `(*CR)` or
`(*LF)` or `(*CRLF)` or `(*ANY)` according to `:re` documentation
* `:dotall` (s) - causes dot to match newlines and also sets newline to
`(*ANYCRLF)`.\
The new line setting, as described in the [`:re` documentation](`:re`),
can be overridden by starting the regular expression pattern with:
* `(*CR)` - carriage return
* `(*LF)` - line feed
* `(*CRLF)` - carriage return, followed by line feed
* `(*ANYCRLF)` - any of the three above
* `(*ANY)` - all Unicode newline sequences
* _Starting from Erlang/OTP 28, `(*NUL)` - the NUL character (binary zero)_
* `:multiline` (m) - causes `^` and `$` to mark the beginning and end of
each line; use `\A` and `\z` to match the end or beginning of the string
@@ -164,6 +173,11 @@ defmodule Regex do
@type t :: %__MODULE__{re_pattern: term, source: binary, opts: [term]}
@type named_captures_opts :: [
return: :binary | :index,
offset: non_neg_integer()
]
defmodule CompileError do
@moduledoc """
An exception raised when a regular expression could not be compiled.
@@ -301,7 +315,7 @@ defmodule Regex do
* `:capture` - what to capture in the result. See the ["Captures" section](#module-captures)
to see the possible capture values.
* `:offset` - (since v1.12.0) specifies the starting offset to match in the given string.
Defaults to zero.
Defaults to `0`.
## Examples
@@ -321,7 +335,7 @@ defmodule Regex do
["d", ""]
"""
@spec run(t, binary, [term]) :: nil | [binary] | [{integer, integer}]
@spec run(t, binary, capture_opts) :: nil | [binary] | [{integer, integer}]
def run(regex, string, options \\ [])
def run(%Regex{} = regex, string, options) when is_binary(string) do
@@ -343,6 +357,8 @@ defmodule Regex do
* `:return` - when set to `:index`, returns byte index and match length.
Defaults to `:binary`.
* `:offset` - (since v1.12.0) specifies the starting offset to match in the given string.
Defaults to `0`.
## Examples
@@ -363,7 +379,7 @@ defmodule Regex do
You can then use `binary_part/3` to fetch the relevant part from the given string.
"""
@spec named_captures(t, String.t(), keyword) :: map | nil
@spec named_captures(t, String.t(), named_captures_opts) :: map | nil
def named_captures(regex, string, options \\ []) when is_binary(string) do
names = names(regex)
options = Keyword.put(options, :capture, names)
@@ -516,7 +532,7 @@ defmodule Regex do
* `:capture` - what to capture in the result. See the ["Captures" section](#module-captures)
to see the possible capture values.
* `:offset` - (since v1.12.0) specifies the starting offset to match in the given string.
Defaults to zero.
Defaults to `0`.
## Examples
@@ -545,7 +561,7 @@ defmodule Regex do
[["cd"], ["ce"]]
"""
@spec scan(t(), String.t(), [term()]) :: [[String.t()]] | [[{integer(), integer()}]]
@spec scan(t(), String.t(), capture_opts) :: [[String.t()]] | [[{integer(), integer()}]]
def scan(regex, string, options \\ [])
def scan(%Regex{} = regex, string, options) when is_binary(string) do
@@ -573,6 +589,25 @@ defmodule Regex do
end
end
@typedoc """
Options for regex functions that capture matches.
"""
@type capture_opts :: [
return: :binary | :index,
capture: :all | :first | :all_but_first | :none | :all_names | [binary() | atom()],
offset: non_neg_integer()
]
@typedoc """
Options for `split/3`.
"""
@type split_opts :: [
parts: pos_integer() | :infinity,
trim: boolean(),
on: :first | :all | :all_but_first | :none | :all_names | [atom() | integer()],
include_captures: boolean()
]
@doc """
Splits the given target based on the given pattern and in the given number of
parts.
@@ -626,7 +661,7 @@ defmodule Regex do
["a", "b", "c"]
"""
@spec split(t, String.t(), [term]) :: [String.t()]
@spec split(t, String.t(), split_opts) :: [String.t()]
def split(regex, string, options \\ [])
def split(%Regex{}, "", opts) do
@@ -974,4 +1009,58 @@ defmodule Regex do
defp translate_options(<<>>, acc), do: acc
defp translate_options(t, _acc), do: {:error, t}
@doc false
def __escape__(%{__struct__: Regex} = regex) do
# OTP 28.0 introduced refs in patterns, which can't be used in AST anymore
# OTP 28.1 introduced :re.import/1 which allows us to work with pre-compiled binaries again
pattern_ast =
cond do
# TODO: Remove this when we require Erlang/OTP 28+
# Before OTP 28.0, patterns did not contain any refs and could be safely be escaped
:erlang.system_info(:otp_release) < [?2, ?8] ->
Macro.escape(regex.re_pattern)
# OTP 28.1+ introduced the ability to export and import regexes from compiled binaries
Code.ensure_loaded?(:re) and function_exported?(:re, :import, 1) ->
{:ok, exported} = :re.compile(regex.source, [:export] ++ regex.opts)
quote do
Regex.__import_pattern__(unquote(Macro.escape(exported)))
end
# we now that the Regex module is defined at this stage, so this macro can be safely called
|> Macro.update_meta(&([required: true] ++ &1))
# TODO: Remove this when we require Erlang/OTP 28.1+
# OTP 28.0 works in degraded mode performance-wise, we need to recompile from the source
true ->
quote do
{:ok, pattern} =
:re.compile(unquote(Macro.escape(regex.source)), unquote(Macro.escape(regex.opts)))
pattern
end
end
quote do
%{
__struct__: unquote(Regex),
re_pattern: unquote(pattern_ast),
source: unquote(Macro.escape(regex.source)),
opts: unquote(Macro.escape(regex.opts))
}
end
end
@doc false
defmacro __import_pattern__(pattern) do
if __CALLER__.context in [:match, :guard] do
raise ArgumentError, "escaped Regex structs are not allowed in match or guards"
end
quote do
:re.import(unquote(pattern))
end
end
end
+131 -36
View File
@@ -187,7 +187,7 @@ defmodule Registry do
Note that the registry uses one ETS table plus two ETS tables per partition.
"""
@keys [:unique, :duplicate]
@keys [:unique, :duplicate, {:duplicate, :key}, {:duplicate, :pid}]
@all_info -1
@key_info -2
@@ -195,7 +195,7 @@ defmodule Registry do
@type registry :: atom
@typedoc "The type of the registry"
@type keys :: :unique | :duplicate
@type keys :: :unique | :duplicate | {:duplicate, :key} | {:duplicate, :pid}
@typedoc "The type of keys allowed on registration"
@type key :: term
@@ -242,6 +242,11 @@ defmodule Registry do
{:register, registry, key, registry_partition :: pid, value}
| {:unregister, registry, key, registry_partition :: pid}
@typedoc """
Options used for `dispatch/4`.
"""
@type dispatch_opts :: [parallel: boolean()]
## Via callbacks
@doc false
@@ -261,8 +266,8 @@ defmodule Registry do
:undefined
end
{kind, _, _} ->
raise ArgumentError, ":via is not supported for #{kind} registries"
{{:duplicate, _}, _, _} ->
raise ArgumentError, ":via is not supported for duplicate registries"
end
end
@@ -324,11 +329,24 @@ defmodule Registry do
{Registry, keys: :unique, name: MyApp.Registry, partitions: System.schedulers_online()}
], strategy: :one_for_one)
For `:duplicate` registries with many different keys (e.g., many topics with
few subscribers each), you can optimize key-based lookups by partitioning by key:
Registry.start_link(
keys: {:duplicate, :key},
name: MyApp.TopicRegistry,
partitions: System.schedulers_online()
)
This allows key-based lookups to check only a single partition instead of
searching all partitions. Use the default `:pid` partitioning when you have
fewer keys with many entries each (e.g., one topic with many subscribers).
## Options
The registry requires the following keys:
* `:keys` - chooses if keys are `:unique` or `:duplicate`
* `:keys` - chooses if keys are `:unique`, `:duplicate`, `{:duplicate, :key}`, or `{:duplicate, :pid}`
* `:name` - the name of the registry and its tables
The following keys are optional:
@@ -340,16 +358,40 @@ defmodule Registry do
crashes. Messages sent to listeners are of type `t:listener_message/0`.
* `:meta` - a keyword list of metadata to be attached to the registry.
For `:duplicate` registries, you can specify the partitioning strategy
directly in the `:keys` option:
* `:duplicate` or `{:duplicate, :pid}` - Use `:pid` partitioning (default)
when you have keys with many entries (e.g., one topic with many subscribers).
This is the traditional behavior and groups all entries from the same process together.
* `{:duplicate, :key}` - Use `:key` partitioning when entries are spread across
many different keys (e.g., many topics with few subscribers each). This makes
key-based lookups more efficient as they only need to check a single partition
instead of all partitions.
"""
@doc since: "1.5.0"
@spec start_link([start_option]) :: {:ok, pid} | {:error, term}
def start_link(options) do
keys = Keyword.get(options, :keys)
if keys not in @keys do
raise ArgumentError,
"expected :keys to be given and be one of :unique or :duplicate, got: #{inspect(keys)}"
end
# Validate and normalize keys format
kind =
case keys do
{:duplicate, partition_strategy} when partition_strategy in [:key, :pid] ->
{:duplicate, partition_strategy}
:unique ->
:unique
:duplicate ->
{:duplicate, :pid}
_ ->
raise ArgumentError,
"expected :keys to be given and be one of :unique, :duplicate, {:duplicate, :key}, or {:duplicate, :pid}, got: #{inspect(keys)}"
end
name =
case Keyword.fetch(options, :name) do
@@ -392,11 +434,18 @@ defmodule Registry do
# The @info format must be kept in sync with Registry.Partition optimization.
entries = [
{@all_info, {keys, partitions, nil, nil, listeners}},
{@key_info, {keys, partitions, nil}} | meta
{@all_info, {kind, partitions, nil, nil, listeners}},
{@key_info, {kind, partitions, nil}} | meta
]
Registry.Supervisor.start_link(keys, name, partitions, listeners, entries, compressed)
Registry.Supervisor.start_link(
kind,
name,
partitions,
listeners,
entries,
compressed
)
end
@doc false
@@ -463,7 +512,8 @@ defmodule Registry do
end
{kind, _, _} ->
raise ArgumentError, "Registry.update_value/3 is not supported for #{kind} registries"
raise ArgumentError,
"Registry.update_value/3 is not supported for #{inspect(kind)} registries"
end
end
@@ -483,9 +533,15 @@ defmodule Registry do
See the module documentation for examples of using the `dispatch/3`
function for building custom dispatching or a pubsub system.
## Options
* `:parallel` - if `true`, the dispatching is done in parallel
across all partitions. Defaults to `false`.
"""
@doc since: "1.4.0"
@spec dispatch(registry, key, dispatcher, keyword) :: :ok
@spec dispatch(registry, key, dispatcher, dispatch_opts) :: :ok
when dispatcher: (entries :: [{pid, value}] -> term) | {module(), atom(), [term()]}
def dispatch(registry, key, mfa_or_fun, opts \\ [])
when is_atom(registry) and is_function(mfa_or_fun, 1)
@@ -497,12 +553,12 @@ defmodule Registry do
|> List.wrap()
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
{:duplicate, 1, key_ets} ->
{{:duplicate, _}, 1, key_ets} ->
key_ets
|> safe_lookup_second(key)
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
{:duplicate, partitions, _} ->
{{:duplicate, _}, partitions, _} ->
if Keyword.get(opts, :parallel, false) do
registry
|> dispatch_parallel(key, mfa_or_fun, partitions)
@@ -614,10 +670,14 @@ defmodule Registry do
[]
end
{:duplicate, 1, key_ets} ->
{{:duplicate, _}, 1, key_ets} ->
safe_lookup_second(key_ets, key)
{:duplicate, partitions, _key_ets} ->
{{:duplicate, :key}, partitions, _key_ets} ->
partition = hash(key, partitions)
safe_lookup_second(key_ets!(registry, partition), key)
{{:duplicate, :pid}, partitions, _key_ets} ->
for partition <- 0..(partitions - 1),
pair <- safe_lookup_second(key_ets!(registry, partition), key),
do: pair
@@ -738,10 +798,10 @@ defmodule Registry do
key_ets = key_ets || key_ets!(registry, key, partitions)
:ets.select(key_ets, spec)
{:duplicate, 1, key_ets} ->
{{:duplicate, _}, 1, key_ets} ->
:ets.select(key_ets, spec)
{:duplicate, partitions, _key_ets} ->
{{:duplicate, _}, partitions, _key_ets} ->
for partition <- 0..(partitions - 1),
pair <- :ets.select(key_ets!(registry, partition), spec),
do: pair
@@ -784,15 +844,34 @@ defmodule Registry do
@spec keys(registry, pid) :: [key]
def keys(registry, pid) when is_atom(registry) and is_pid(pid) do
{kind, partitions, _, pid_ets, _} = info!(registry)
{_, pid_ets} = pid_ets || pid_ets!(registry, pid, partitions)
pid_etses =
if pid_ets do
{_, pid_ets} = pid_ets
[pid_ets]
else
case kind do
{:duplicate, :key} ->
for partition <- 0..(partitions - 1) do
{_, pid_ets} = pid_ets!(registry, partition)
pid_ets
end
_ ->
{_, pid_ets} = pid_ets!(registry, pid, partitions)
[pid_ets]
end
end
keys =
try do
spec = [{{pid, :"$1", :"$2", :_}, [], [{{:"$1", :"$2"}}]}]
:ets.select(pid_ets, spec)
catch
:error, :badarg -> []
end
Enum.flat_map(pid_etses, fn pid_ets ->
try do
spec = [{{pid, :"$1", :"$2", :_}, [], [{{:"$1", :"$2"}}]}]
:ets.select(pid_ets, spec)
catch
:error, :badarg -> []
end
end)
# Handle the possibility of fake keys
keys = gather_keys(keys, [], false)
@@ -871,8 +950,17 @@ defmodule Registry do
[]
end
{:duplicate, partitions, key_ets} ->
key_ets = key_ets || key_ets!(registry, pid, partitions)
{{:duplicate, _}, 1, key_ets} ->
for {^pid, value} <- safe_lookup_second(key_ets, key), do: value
{{:duplicate, :key}, partitions, _key_ets} ->
partition = hash(key, partitions)
key_ets = key_ets!(registry, partition)
for {^pid, value} <- safe_lookup_second(key_ets, key), do: value
{{:duplicate, :pid}, partitions, _key_ets} ->
partition = hash(pid, partitions)
key_ets = key_ets!(registry, partition)
for {^pid, value} <- safe_lookup_second(key_ets, key), do: value
end
end
@@ -1110,7 +1198,7 @@ defmodule Registry do
end
end
defp register_key(:duplicate, key_ets, _key, entry) do
defp register_key({:duplicate, _}, key_ets, _key, entry) do
true = :ets.insert(key_ets, entry)
:ok
end
@@ -1328,10 +1416,10 @@ defmodule Registry do
key_ets = key_ets || key_ets!(registry, key, partitions)
:ets.select_count(key_ets, spec)
{:duplicate, 1, key_ets} ->
{{:duplicate, _}, 1, key_ets} ->
:ets.select_count(key_ets, spec)
{:duplicate, partitions, _key_ets} ->
{{:duplicate, _}, partitions, _key_ets} ->
Enum.sum_by(0..(partitions - 1), fn partition_index ->
:ets.select_count(key_ets!(registry, partition_index), spec)
end)
@@ -1501,7 +1589,12 @@ defmodule Registry do
{hash(key, partitions), hash(pid, partitions)}
end
defp partitions(:duplicate, _key, pid, partitions) do
defp partitions({:duplicate, :key}, key, _pid, partitions) do
partition = hash(key, partitions)
{partition, partition}
end
defp partitions({:duplicate, :pid}, _key, pid, partitions) do
partition = hash(pid, partitions)
{partition, partition}
end
@@ -1565,9 +1658,10 @@ defmodule Registry.Supervisor do
defp strategy_for_kind(:unique), do: :one_for_all
# Duplicate registries have both key and pid partitions hashed
# by pid. This means that, if a PID partition crashes, all of
# by key ({:duplicate, :key}) or pid ({:duplicate, :pid}).
# This means that, if a PID or key partition crashes, all of
# its associated entries are in its sibling table, so we crash one.
defp strategy_for_kind(:duplicate), do: :one_for_one
defp strategy_for_kind({:duplicate, _}), do: :one_for_one
end
defmodule Registry.Partition do
@@ -1622,6 +1716,7 @@ defmodule Registry.Partition do
def init({kind, registry, i, partitions, key_partition, pid_partition, listeners, compressed}) do
Process.flag(:trap_exit, true)
key_ets = init_key_ets(kind, key_partition, compressed)
pid_ets = init_pid_ets(kind, pid_partition)
@@ -1648,7 +1743,7 @@ defmodule Registry.Partition do
:ets.new(key_partition, compression_opt(opts, compressed))
end
defp init_key_ets(:duplicate, key_partition, compressed) do
defp init_key_ets({:duplicate, _}, key_partition, compressed) do
opts = [:duplicate_bag, :public, read_concurrency: true, write_concurrency: true]
:ets.new(key_partition, compression_opt(opts, compressed))
end
+14 -4
View File
@@ -22,7 +22,7 @@ defmodule String do
"hello world"
The functions in this module act according to
[The Unicode Standard, Version 16.0.0](http://www.unicode.org/versions/Unicode16.0.0/).
[The Unicode Standard, Version 17.0.0](http://www.unicode.org/versions/Unicode17.0.0/).
## Interpolation
@@ -298,6 +298,15 @@ defmodule String do
| [nonempty_binary]
| (compiled_search_pattern :: :binary.cp())
@type split_opts :: [
parts: pos_integer() | :infinity,
trim: boolean()
]
@type splitter_opts :: [trim: boolean()]
@type replace_opts :: [global: boolean()]
@conditional_mappings [:greek, :turkic]
@doc """
@@ -502,7 +511,8 @@ defmodule String do
["a", "b", " c "]
"""
@spec split(t, pattern | Regex.t(), keyword) :: [t]
@spec split(t, pattern, split_opts()) :: [t]
@spec split(t, Regex.t(), Regex.split_opts()) :: [t]
def split(string, pattern, options \\ [])
def split(string, %Regex{} = pattern, options) when is_binary(string) and is_list(options) do
@@ -607,7 +617,7 @@ defmodule String do
["1", "2", "3", "4"]
"""
@spec splitter(t, pattern, keyword) :: Enumerable.t()
@spec splitter(t, pattern, splitter_opts) :: Enumerable.t()
def splitter(string, pattern, options \\ [])
def splitter(string, "", options) when is_binary(string) and is_list(options) do
@@ -1616,7 +1626,7 @@ defmodule String do
"é"
"""
@spec replace(t, pattern | Regex.t(), t | (t -> t | iodata), keyword) :: t
@spec replace(t, pattern | Regex.t(), t | (t -> t | iodata), replace_opts) :: t
def replace(subject, pattern, replacement, options \\ [])
when is_binary(subject) and
(is_binary(replacement) or is_function(replacement, 1)) and
+9 -3
View File
@@ -17,6 +17,11 @@ defmodule StringIO do
"""
@type open_opts :: [
capture_prompt: boolean(),
encoding: :unicode | :latin1
]
# We're implementing the GenServer behaviour instead of using the
# `use GenServer` macro, because we don't want the `child_spec/1`
# function as it doesn't make sense to be started under a supervisor.
@@ -59,7 +64,7 @@ defmodule StringIO do
"""
@doc since: "1.7.0"
@spec open(binary, keyword, (pid -> res)) :: {:ok, res} when res: var
@spec open(binary, open_opts, (pid -> res)) :: {:ok, res} when res: var
def open(string, options, function)
when is_binary(string) and is_list(options) and is_function(function, 1) do
{:ok, pid} = GenServer.start(__MODULE__, {self(), string, options}, [])
@@ -83,7 +88,8 @@ defmodule StringIO do
If options are provided, the result will be `{:ok, pid}`, returning the
IO device created. The option `:capture_prompt`, when set to `true`, causes
prompts (which are specified as arguments to `IO.get*` functions) to be
included in the device's output.
included in the device's output. See `options/3` for the list of supported
options.
If a function is provided, the device will be created and sent to the
function. When the function returns, the device will be closed. The final
@@ -111,7 +117,7 @@ defmodule StringIO do
{:ok, {"", "The input was foo"}}
"""
@spec open(binary, keyword) :: {:ok, pid}
@spec open(binary, open_opts) :: {:ok, pid}
@spec open(binary, (pid -> res)) :: {:ok, res} when res: var
def open(string, options_or_function \\ [])
+14 -1
View File
@@ -659,6 +659,19 @@ defmodule Supervisor do
@typedoc since: "1.16.0"
@type module_spec :: {module(), args :: term()} | module()
@typedoc """
Options for overriding child specification fields.
"""
@type child_spec_overrides :: [
id: atom() | term(),
start: {module(), atom(), [term()]},
restart: restart(),
shutdown: shutdown(),
type: type(),
modules: [module()] | :dynamic,
significant: boolean()
]
@doc """
Starts a supervisor with the given children.
@@ -896,7 +909,7 @@ defmodule Supervisor do
#=> start: {Agent, :start_link, [fn -> :ok end]}}
"""
@spec child_spec(child_spec() | module_spec(), keyword()) :: child_spec()
@spec child_spec(child_spec() | module_spec(), child_spec_overrides()) :: child_spec()
def child_spec(module_or_map, overrides)
def child_spec({_, _, _, _, _, _} = tuple, _overrides) do
+25 -3
View File
@@ -113,6 +113,28 @@ defmodule System do
| :sigusr1
| :sigusr2
@type cmd_opts :: [
into: Collectable.t(),
lines: pos_integer(),
cd: Path.t(),
env: [{binary(), binary() | nil}],
arg0: binary(),
stderr_to_stdout: boolean(),
use_stdio: boolean(),
parallelism: boolean()
]
@type shell_opts :: [
into: Collectable.t(),
lines: pos_integer(),
cd: Path.t(),
env: [{binary(), binary() | nil}],
stderr_to_stdout: boolean(),
use_stdio: boolean(),
parallelism: boolean(),
close_stdin: boolean()
]
@vm_signals [:sigquit, :sigterm, :sigusr1]
@os_signals [:sighup, :sigabrt, :sigalrm, :sigusr2, :sigchld, :sigstop, :sigtstp]
@signals @vm_signals ++ @os_signals
@@ -938,10 +960,10 @@ defmodule System do
* `:close_stdin` (since v1.14.1) - if the stdin should be closed
on Unix systems, forcing any command that waits on stdin to
immediately terminate. Defaults to false.
immediately terminate. Defaults to `false`.
"""
@doc since: "1.12.0"
@spec shell(binary, keyword) :: {Collectable.t(), exit_status :: non_neg_integer}
@spec shell(binary, shell_opts) :: {Collectable.t(), exit_status :: non_neg_integer}
def shell(command, opts \\ []) when is_binary(command) do
command |> String.trim() |> do_shell(opts)
end
@@ -1101,7 +1123,7 @@ defmodule System do
If you desire to execute a trusted command inside a shell, with pipes,
redirecting and so on, please check `shell/2`.
"""
@spec cmd(binary, [binary], keyword) :: {Collectable.t(), exit_status :: non_neg_integer}
@spec cmd(binary, [binary], cmd_opts) :: {Collectable.t(), exit_status :: non_neg_integer}
def cmd(command, args, opts \\ []) when is_binary(command) and is_list(args) do
assert_no_null_byte!(command, "System.cmd/3")
+13 -6
View File
@@ -448,17 +448,17 @@ defmodule Task do
the code before async/await has the same properties after you
add the async call. For example, imagine you have this:
x = heavy_fun()
y = some_fun()
x = heavy_function()
y = some_function()
x + y
Now you want to make the `heavy_fun()` async:
Now you want to make the `heavy_function()` async:
x = Task.async(&heavy_fun/0)
y = some_fun()
x = Task.async(&heavy_function/0)
y = some_function()
Task.await(x) + y
As before, if `heavy_fun/0` fails, the whole computation will
As before, if `heavy_function/0` fails, the whole computation will
fail, including the caller process. If you don't want the task
to fail then you must change the `heavy_fun/0` code in the
same way you would achieve it if you didn't have the async call.
@@ -1444,6 +1444,13 @@ defmodule Task do
end
end
# exported only to avoid dialyzer opaqueness check in internal Task modules
@doc false
@spec __alias__(pid()) :: Task.ref()
def __alias__(pid) do
build_alias(pid)
end
## Optimizations
defp build_monitor(pid) do
+19 -7
View File
@@ -87,6 +87,18 @@ defmodule Task.Supervisor do
@typedoc since: "1.17.0"
@type async_stream_option :: Task.async_stream_option() | {:shutdown, Supervisor.shutdown()}
@typedoc """
Options for `async/3`, `async/5`, `async_nolink/3`, and `async_nolink/5` functions.
"""
@type async_opts :: [
shutdown: :brutal_kill | timeout()
]
@type start_child_opts :: [
restart: :temporary | :transient | :permanent,
shutdown: :brutal_kill | timeout()
]
@doc false
def child_spec(opts) when is_list(opts) do
id =
@@ -175,7 +187,7 @@ defmodule Task.Supervisor do
The tasks must trap exits for the timeout to have an effect.
"""
@spec async(Supervisor.supervisor(), (-> any), Keyword.t()) :: Task.t()
@spec async(Supervisor.supervisor(), (-> any), async_opts) :: Task.t()
def async(supervisor, fun, options \\ []) do
async(supervisor, :erlang, :apply, [fun, []], options)
end
@@ -197,7 +209,7 @@ defmodule Task.Supervisor do
The tasks must trap exits for the timeout to have an effect.
"""
@spec async(Supervisor.supervisor(), module, atom, [term], Keyword.t()) :: Task.t()
@spec async(Supervisor.supervisor(), module, atom, [term], async_opts) :: Task.t()
def async(supervisor, module, fun, args, options \\ []) do
async(supervisor, :link, module, fun, args, options)
end
@@ -284,7 +296,7 @@ defmodule Task.Supervisor do
end
"""
@spec async_nolink(Supervisor.supervisor(), (-> any), Keyword.t()) :: Task.t()
@spec async_nolink(Supervisor.supervisor(), (-> any), async_opts) :: Task.t()
def async_nolink(supervisor, fun, options \\ []) do
async_nolink(supervisor, :erlang, :apply, [fun, []], options)
end
@@ -303,7 +315,7 @@ defmodule Task.Supervisor do
as the `:restart` option (the default), as `async_nolink/5` keeps a
direct reference to the task which is lost if the task is restarted.
"""
@spec async_nolink(Supervisor.supervisor(), module, atom, [term], Keyword.t()) :: Task.t()
@spec async_nolink(Supervisor.supervisor(), module, atom, [term], async_opts) :: Task.t()
def async_nolink(supervisor, module, fun, args, options \\ []) do
async(supervisor, :nolink, module, fun, args, options)
end
@@ -523,7 +535,7 @@ defmodule Task.Supervisor do
The task must trap exits for the timeout to have an effect.
"""
@spec start_child(Supervisor.supervisor(), (-> any), keyword) ::
@spec start_child(Supervisor.supervisor(), (-> any), start_child_opts) ::
DynamicSupervisor.on_start_child()
def start_child(supervisor, fun, options \\ []) do
restart = options[:restart]
@@ -538,7 +550,7 @@ defmodule Task.Supervisor do
Similar to `start_child/3` except the task is specified
by the given `module`, `fun` and `args`.
"""
@spec start_child(Supervisor.supervisor(), module, atom, [term], keyword) ::
@spec start_child(Supervisor.supervisor(), module, atom, [term], start_child_opts) ::
DynamicSupervisor.on_start_child()
def start_child(supervisor, module, fun, args, options \\ [])
when is_atom(fun) and is_list(args) do
@@ -602,7 +614,7 @@ defmodule Task.Supervisor do
case start_child_with_spec(supervisor, [get_owner(owner), :monitor], :temporary, shutdown) do
{:ok, pid} ->
if link_type == :link, do: Process.link(pid)
alias = :erlang.monitor(:process, pid, alias: :demonitor)
alias = Task.__alias__(pid)
send(pid, {owner, alias, alias, get_callers(owner), {module, fun, args}})
%Task{pid: pid, ref: alias, owner: owner, mfa: {module, fun, length(args)}}
+5 -3
View File
@@ -118,6 +118,8 @@ defmodule Version do
@type build :: String.t() | nil
@type t :: %__MODULE__{major: major, minor: minor, patch: patch, pre: pre, build: build}
@type match_opts :: [allow_pre: boolean()]
defmodule Requirement do
@moduledoc """
A struct that holds version requirement information.
@@ -296,7 +298,7 @@ defmodule Version do
** (Version.InvalidRequirementError) invalid requirement: "== == 1.0.0"
"""
@spec match?(version, requirement, keyword) :: boolean
@spec match?(version, requirement, match_opts) :: boolean
def match?(version, requirement, opts \\ [])
def match?(version, requirement, opts) when is_binary(requirement) do
@@ -440,7 +442,7 @@ defmodule Version do
If `string` is an invalid requirement, a `Version.InvalidRequirementError` is raised.
# Examples
## Examples
iex> Version.parse_requirement!("== 2.0.1")
Version.parse_requirement!("== 2.0.1")
@@ -483,7 +485,7 @@ defmodule Version do
@doc """
Converts the given version to a string.
### Examples
## Examples
iex> Version.to_string(%Version{major: 1, minor: 2, patch: 3})
"1.2.3"
@@ -144,7 +144,7 @@ end
#### Problem
An `Atom` is an Elixir basic type whose value is its own name. Atoms are often useful to identify resources or express the state, or result, of an operation. Creating atoms dynamically is not an anti-pattern by itself. However, atoms are not garbage collected by the Erlang Virtual Machine, so values of this type live in memory during a software's entire execution lifetime. The Erlang VM limits the number of atoms that can exist in an application by default to *1_048_576*, which is more than enough to cover all atoms defined in a program, but attempts to serve as an early limit for applications which are "leaking atoms" through dynamic creation.
An `Atom` is an Elixir basic type whose value is its own name. Atoms are often useful to identify resources or express the state, or result, of an operation. Creating atoms dynamically is not an anti-pattern by itself. However, atoms are not garbage collected by the Erlang Virtual Machine, so values of this type live in memory during a software's entire execution lifetime. The Erlang VM limits the number of atoms that can exist in an application by default to *1 048 576*, which is more than enough to cover all atoms defined in a program, but attempts to serve as an early limit for applications which are "leaking atoms" through dynamic creation.
For these reasons, creating atoms dynamically can be considered an anti-pattern when the developer has no control over how many atoms will be created during the software execution. This unpredictable scenario can expose the software to unexpected behavior caused by excessive memory usage, or even by reaching the maximum number of *atoms* possible.
@@ -292,7 +292,7 @@ For convenience, the markup notation to generate the admonition block above is t
#### Problem
This anti-pattern is the opposite of ["Compile-time dependencies"](#compile-time-dependencies) and it happens when a compile-time dependency is accidentally bypassed, making the Elixir compiler is to track dependencies and recompile files correctly. This happens when building aliases (in other words, module names) dynamically, either within a module or within a macro.
This anti-pattern is the opposite of ["Compile-time dependencies"](#compile-time-dependencies) and it happens when a compile-time dependency is accidentally bypassed, making the Elixir compiler unable to track dependencies and recompile files correctly. This happens when building aliases (in other words, module names) dynamically, either within a module or within a macro.
#### Example
@@ -320,7 +320,7 @@ end
In the previous example, even though Elixir does not know which modules the function `example/0` was invoked on, it knows the modules `OtherModule.Foo` and `OtherModule.Bar` are referred outside of a function and therefore they become compile-time dependencies. If any of them change, Elixir will recompile `MyModule` itself.
However, you should not programatically generate the module names themselves, as that would make it impossible for Elixir to track them. More precisely, do not do this:
However, you should not programmatically generate the module names themselves, as that would make it impossible for Elixir to track them. More precisely, do not do this:
```elixir
defmodule MyModule do
@@ -348,7 +348,7 @@ end
#### Refactoring
To address this anti-pattern, you should avoid defining module names programatically. For example, if you need to dispatch to multiple modules, do so by using full module names.
To address this anti-pattern, you should avoid defining module names programmatically. For example, if you need to dispatch to multiple modules, do so by using full module names.
Instead of:
@@ -251,71 +251,55 @@ GenServer.cast(pid, {:report_ip_address, conn.remote_ip})
#### Problem
In Elixir, creating a process outside a supervision tree is not an anti-pattern in itself. However, when you spawn many long-running processes outside of supervision trees, this can make visibility and monitoring of these processes difficult, preventing developers from fully controlling their applications.
In Elixir, creating a process outside a supervision tree is not an anti-pattern in itself. However, when you spawn many long-running processes outside of supervision trees, this can make visibility and monitoring of these processes difficult, preventing developers from fully controlling their lifecycle.
#### Example
The following code example seeks to illustrate a library responsible for maintaining a numerical `Counter` through a `GenServer` process *outside a supervision tree*. Multiple counters can be created simultaneously by a client (one process for each counter), making these *unsupervised* processes difficult to manage. This can cause problems with the initialization, restart, and shutdown of a system.
The following code example seeks to illustrate a library responsible for maintaining a numerical `Counter` through a `Agent` process *outside a supervision tree*.
```elixir
defmodule Counter do
@moduledoc """
Global counter implemented through a GenServer process.
Global counter implemented as an Agent.
"""
use GenServer
use Agent
@doc "Starts a counter process."
def start_link(opts \\ []) do
initial_value = Keyword.get(opts, :initial_value, 0)
initial_state = Keyword.get(opts, :initial_value, 0)
name = Keyword.get(opts, :name, __MODULE__)
GenServer.start(__MODULE__, initial_value, name: name)
Agent.start_link(fn -> initial_state end, name: name)
end
@doc "Gets the current value of the given counter."
def get(pid_name \\ __MODULE__) do
GenServer.call(pid_name, :get)
def get(name \\ __MODULE__) do
Agent.get(name, fn state -> state end)
end
@doc "Bumps the value of the given counter."
def bump(pid_name \\ __MODULE__, value) do
GenServer.call(pid_name, {:bump, value})
end
@impl true
def init(counter) do
{:ok, counter}
end
@impl true
def handle_call(:get, _from, counter) do
{:reply, counter, counter}
end
def handle_call({:bump, value}, _from, counter) do
{:reply, counter, counter + value}
def bump(name \\ __MODULE__, value) do
Agent.get_and_update(fn state -> {state, value + state} end)
end
end
```
While it is possible to start the process outside of a supervision tree:
```elixir
iex> Counter.start_link()
{:ok, #PID<0.115.0>}
iex> Counter.get()
iex> Counter.bump(13)
0
iex> Counter.start_link(initial_value: 15, name: :other_counter)
{:ok, #PID<0.120.0>}
iex> Counter.get(:other_counter)
15
iex> Counter.bump(:other_counter, -3)
12
iex> Counter.bump(Counter, 7)
7
iex> Counter.get()
13
```
Such processes are harder to observe and control their lifecycle. For example, if you have other processes that depend on the `Counter` above, you will need ad-hoc mechanisms to make sure they are initialized in order. Furthermore, when your application is shutting down, there is no guarantee when they are terminated.
#### Refactoring
To ensure that clients of a library have full control over their systems, regardless of the number of processes used and the lifetime of each one, all processes must be started inside a supervision tree. As shown below, this code uses a `Supervisor` as a supervision tree. When this Elixir application is started, two different counters (`Counter` and `:other_counter`) are also started as child processes of the `Supervisor` named `App.Supervisor`. One is initialized with `0`, the other with `15`. By means of this supervision tree, it is possible to manage the life cycle of all child processes (stopping or restarting each one), improving the visibility of the entire app.
To ensure that clients of a library have full control over their systems, regardless of the number of processes used and the lifetime of each one, all processes must be started inside a supervision tree. As shown below, this code uses a `Supervisor` as a supervision tree.
```elixir
defmodule SupervisedProcess.Application do
@@ -338,21 +322,8 @@ defmodule SupervisedProcess.Application do
end
```
```elixir
iex> Supervisor.count_children(App.Supervisor)
%{active: 2, specs: 2, supervisors: 0, workers: 2}
iex> Counter.get(Counter)
0
iex> Counter.get(:other_counter)
15
iex> Counter.bump(Counter, 7)
7
iex> Supervisor.terminate_child(App.Supervisor, Counter)
iex> Supervisor.count_children(App.Supervisor) # Only one active child
%{active: 1, specs: 2, supervisors: 0, workers: 2}
iex> Counter.get(Counter) # The process was terminated
** (EXIT) no process: the process is not alive...
iex> Supervisor.restart_child(App.Supervisor, Counter)
iex> Counter.get(Counter) # After the restart, this process can be used again
0
```
Besides having a deterministic order in which processes are started, supervision trees also guarantee they are terminated in reverse order, allowing you to perform any necessary clean up during shut down. Furthermore, supervision strategies allows us to configure exactly how process should act in case of unexpected failures.
Finally, applications and supervision trees can be introspected through applications like the [Phoenix.LiveDashboard](http://github.com/phoenixframework/phoenix_live_dashboard) and [Erlang's built-in observer](https://www.erlang.org/doc/apps/observer/observer_ug):
<img src="assets/kv-observer.png" alt="Observer GUI screenshot" />
@@ -156,83 +156,6 @@ end
Since `use` allows any code to run, we can't really know the side-effects of using a module without reading its documentation. Therefore use this function with care and only if strictly required. Don't use `use` where an `import` or `alias` would do.
## Understanding Aliases
At this point, you may be wondering: what exactly is an Elixir alias and how is it represented?
An alias in Elixir is a capitalized identifier (like `String`, `Keyword`, etc) which is converted to an atom during compilation. For instance, the `String` alias translates by default to the atom `:"Elixir.String"`:
```elixir
iex> is_atom(String)
true
iex> to_string(String)
"Elixir.String"
iex> :"Elixir.String" == String
true
```
By using the `alias/2` directive, we are changing the atom the alias expands to.
Aliases expand to atoms because in the Erlang Virtual Machine (and consequently Elixir) modules are always represented by atoms:
```elixir
iex> List.flatten([1, [2], 3])
[1, 2, 3]
iex> :"Elixir.List".flatten([1, [2], 3])
[1, 2, 3]
```
That's the mechanism we use to call Erlang modules:
```elixir
iex> :lists.flatten([1, [2], 3])
[1, 2, 3]
```
## Module nesting
Now that we have talked about aliases, we can talk about nesting and how it works in Elixir. Consider the following example:
```elixir
defmodule Foo do
defmodule Bar do
end
end
```
The example above will define two modules: `Foo` and `Foo.Bar`. The second can be accessed as `Bar` inside `Foo` as long as they are in the same lexical scope.
If, later, the `Bar` module is moved outside the `Foo` module definition, it must be referenced by its full name (`Foo.Bar`) or an alias must be set using the `alias` directive discussed above.
**Note**: in Elixir, you don't have to define the `Foo` module before being able to define the `Foo.Bar` module, as they are effectively independent. The above could also be written as:
```elixir
defmodule Foo.Bar do
end
defmodule Foo do
alias Foo.Bar
# Can still access it as `Bar`
end
```
Aliasing a nested module does not bring parent modules into scope. Consider the following example:
```elixir
defmodule Foo do
defmodule Bar do
defmodule Baz do
end
end
end
alias Foo.Bar.Baz
# The module `Foo.Bar.Baz` is now available as `Baz`
# However, the module `Foo.Bar` is *not* available as `Bar`
```
As we will see in later chapters, aliases also play a crucial role in macros, to guarantee they are hygienic.
## Multi alias/import/require/use
It is possible to `alias`, `import`, `require`, or `use` multiple modules at once. This is particularly useful once we start nesting modules, which is very common when building Elixir applications. For example, imagine you have an application where all modules are nested under `MyApp`, you can alias the modules `MyApp.Foo`, `MyApp.Bar` and `MyApp.Baz` at once as follows:
@@ -241,4 +164,4 @@ It is possible to `alias`, `import`, `require`, or `use` multiple modules at onc
alias MyApp.{Foo, Bar, Baz}
```
With this, we have finished our tour of Elixir modules. The next topic to cover is module attributes.
With this, we have finished our tour of Elixir modules.
@@ -9,7 +9,7 @@ Anonymous functions allow us to store and pass executable code around as if it w
## Identifying functions and documentation
Before we move on to discuss anonymous functions, let's talk about how Elixir identifies named functions.
Before we move on to discuss anonymous functions, let's talk about how Elixir identifies named functions – the functions defined in [modules](modules-and-functions.md).
Functions in Elixir are identified by both their name and their arity. The arity of a function describes the number of arguments that the function takes. From this point on we will use both the function name and its arity to describe functions throughout the documentation. `trunc/1` identifies the function which is named `trunc` and takes `1` argument, whereas `trunc/2` identifies a different (nonexistent) function with the same name but with an arity of `2`.
@@ -100,7 +100,11 @@ iex> if nil do
"This will"
```
This is also a good opportunity to talk about variable scoping in Elixir. If any variable is declared or changed inside [`if`](`if/2`), [`case`](`case/2`), and similar constructs, the declaration and change will only be visible inside the construct. For example:
### Expressions
Some programming languages make a distinction about expressions (code that returns a value) and statements (code that returns no value). In Elixir, there are only expressions, no statements. Everything you write in Elixir language returns some value.
This property allows variables to be scoped to individual blocks of code such as [`if`](`if/2`), [`case`](`case/2`), where declarations or changes are only visible inside the block. A change can't leak to outer blocks, which makes code easier to follow and understand. For example:
```elixir
iex> x = 1
@@ -113,19 +117,22 @@ iex> x
1
```
In said cases, if you want to change a value, you must return the value from the [`if`](`if/2`):
You see the return value of the [`if`](`if/2`) expression as the resulting `2` here. To retain changes made within the [`if`](`if/2`) expression on the outer block you need to assign the returned value to a variable in the outer block.
```elixir
iex> x = 1
1
iex> x = if true do
...> x + 1
...> else
...> x
...> end
iex> x =
...> if true do
...> x + 1
...> else
...> x
...> end
2
```
With all expressions returning a value there's also no need for alternative constructs, such as ternary operators posing as an alternative to [`if`](`if/2`). Elixir does include an inline notation for [`if`](`if/2`) and, as we will [learn later](keywords-and-maps.md#do-blocks-and-keywords), it is a syntactic variation on `if`'s arguments.
> #### `if` is a macro {: .info}
>
> An interesting note regarding [`if`](`if/2`) is that it is implemented as a macro in the language: it isn't a special language construct as it would be in many languages. You can check the documentation and its source for more information.
@@ -50,13 +50,13 @@ after: [2, 4, 6]
It is also very common to use `IO.inspect/2` with `binding/0`, which returns all variable names and their values:
```elixir
def some_fun(a, b, c) do
def some_function(a, b, c) do
IO.inspect(binding())
...
end
```
When `some_fun/3` is invoked with `:foo`, `"bar"`, `:baz` it prints:
When `some_function/3` is invoked with `:foo`, `"bar"`, `:baz` it prints:
```elixir
[a: :foo, b: "bar", c: :baz]
@@ -77,7 +77,7 @@ dbg(Map.put(feature, :in_version, "1.14.0"))
The code above prints this:
```shell
```text
[my_file.exs:2: (file)]
feature #=> %{inspiration: "Rust", name: :dbg}
[my_file.exs:3: (file)]
@@ -97,7 +97,7 @@ __ENV__.file
This code prints:
```shell
```text
[dbg_pipes.exs:5: (file)]
__ENV__.file #=> "/home/myuser/dbg_pipes.exs"
|> String.split("/", trim: true) #=> ["home", "myuser", "dbg_pipes.exs"]
@@ -85,7 +85,7 @@ iex> :digraph.get_short_path(digraph, v0, v2)
Note that the functions in `:digraph` alter the graph structure in-place, this
is possible because they are implemented as ETS tables, explained next.
## Erlang Term Storage
## Erlang Term Storage (ETS)
The modules [`:ets`](`:ets`) and [`:dets`](`:dets`) handle storage of large data structures in memory or on disk respectively.
@@ -57,4 +57,4 @@ $ elixir simple.exs
Hello world from Elixir
```
Later on we will learn [how to compile Elixir code](modules-and-functions.md) and how to create and work within Elixir projects using the Mix build tool. For now, let's move on to learn the basic data types in the language.
`iex` and `elixir` are all we need to learn the main language concepts. There is a separate guide named ["Mix and OTP guide"](../mix-and-otp/introduction-to-mix.md) that explores how to actually create, manage, and test full-blown Elixir projects. For now, let's move on to learn the basic data types in the language.
@@ -131,7 +131,7 @@ iex> if true do
In the example above, the `do` and `else` blocks make up a keyword list. They are nothing more than a syntax convenience on top of keyword lists. We can rewrite the above to:
```elixir
iex> if true, do: "This will be seen", else: "This won't"
iex> if(true, do: "This will be seen", else: "This won't")
"This will be seen"
```
@@ -225,6 +225,8 @@ These operations have one large benefit in that they raise if the key does not e
Elixir developers typically prefer to use the `map.key` syntax and pattern matching instead of the functions in the `Map` module when working with maps because they lead to an assertive style of programming. [This blog post by José Valim](https://dashbit.co/blog/writing-assertive-code-with-elixir) provides insight and examples on how you get more concise and faster software by writing assertive code in Elixir.
In a further chapter you'll learn about ["Structs"](structs.md), which further enforce the idea of a map with predefined keys.
## Nested data structures
Often we will have maps inside maps, or even keywords lists inside maps, and so forth. Elixir provides conveniences for manipulating nested data structures via the `get_in/1`, `put_in/2`, `update_in/2`, and other macros giving the same conveniences you would find in imperative languages while keeping the immutable properties of the language.
@@ -32,7 +32,7 @@ In the example above, we are defining the module documentation by using the modu
`@moduledoc` and `@doc` are by far the most used attributes, and we expect you to use them a lot. Elixir treats documentation as first-class and provides many functions to access documentation. We will cover them [in their own chapter](writing-documentation.md).
Let's go back to the `Math` module defined in the previous chapters, add some documentation and save it to the `math.ex` file:
Documentation is only accessible from compiled modules. So in order to give it a try, let's once again define the `Math` module, but this time within a file named `math.ex`:
```elixir
defmodule Math do
@@ -53,23 +53,29 @@ defmodule Math do
end
```
Elixir promotes the use of Markdown with heredocs to write readable documentation. Heredocs are multi-line strings, they start and end with triple double-quotes, keeping the formatting of the inner text. We can access the documentation of any compiled module directly from IEx:
Elixir promotes the use of Markdown with heredocs to write readable documentation. Heredocs are multi-line strings, they start and end with triple double-quotes, keeping the formatting of the inner text.
```console
$ elixirc math.ex
$ iex
```
Now let's compile it. Start `iex` and then invoke [the `c/2` helper](`IEx.Helpers.c/2`):
```elixir
iex> h Math # Access the docs for the module Math
iex> c("math.ex", ".")
[Math]
```
And now we can access them:
```elixir
iex> h Math # Docs for module Math
...
iex> h Math.sum # Access the docs for the sum function
iex> h Math.sum # Docs for the sum function
...
```
We also provide a tool called [ExDoc](https://github.com/elixir-lang/ex_doc) which is used to generate HTML pages from the documentation.
When we compiled the module, you may have noticed Elixir created a `Elixir.Math.beam` file. That's the bytecode for the module and that's where the documentation is stored.
You can take a look at the docs for `Module` for a complete list of supported attributes. Elixir also uses attributes to annotate our code with [typespecs](../references/typespecs.md).
In our day to day, Elixir developers use the `Mix` build tool to compile code and projects like [ExDoc](https://github.com/elixir-lang/ex_doc) to generate HTML and EPUB pages from the documentation.
Take a look at the docs for `Module` for a complete list of supported attributes.
## As temporary storage
@@ -12,7 +12,7 @@ iex> String.length("hello")
5
```
In order to create our own modules in Elixir, we use the [`defmodule`](`defmodule/2`) macro. The first letter of the module must be in uppercase. We use the [`def`](`def/2`) macro to define functions in that module. The first letter of every function must be in lowercase (or underscore):
In order to create our own modules in Elixir, we use the [`defmodule`](`defmodule/2`) macro. The first letter of an module name (an alias, as described further down) must be in uppercase. We use the [`def`](`def/2`) macro to define functions in that module. The first letter of every function must be in lowercase (or underscore):
```elixir
iex> defmodule Math do
@@ -25,38 +25,13 @@ iex> Math.sum(1, 2)
3
```
In this chapter we will define our own modules, with different levels of complexity. As our examples get longer in size, it can be tricky to type them all in the shell. It's about time for us to learn how to compile Elixir code and also how to run Elixir scripts.
In this chapter we will define our own modules, with different levels of complexity. As our examples get longer in size, it can be tricky to type them all in the shell, so we will resort more frequently to scripting.
## Compilation
## Scripting
Most of the time it is convenient to write modules into files so they can be compiled and reused. Let's assume we have a file named `math.ex` with the following contents:
Elixir has two file extensions `.ex` (Elixir) and `.exs` (Elixir scripts). Elixir treats both files exactly the same way, the only difference is in intention. `.ex` files are meant to be compiled while `.exs` files are used for scripting.
```elixir
defmodule Math do
def sum(a, b) do
a + b
end
end
```
This file can be compiled using `elixirc`:
```console
$ elixirc math.ex
```
This will generate a file named `Elixir.Math.beam` containing the bytecode for the defined module. If we start `iex` again, our module definition will be available (provided that `iex` is started in the same directory the bytecode file is in):
```elixir
iex> Math.sum(1, 2)
3
```
## Scripting mode
In addition to the Elixir file extension `.ex`, Elixir also supports `.exs` files for scripting. Elixir treats both files exactly the same way, the only difference is in intention. `.ex` files are meant to be compiled while `.exs` files are used for scripting. This convention is followed by projects like `mix`.
For instance, we can create a file called `math.exs`:
Let's create a file named `math.exs`:
```elixir
defmodule Math do
@@ -74,15 +49,13 @@ And execute it as:
$ elixir math.exs
```
Because we used `elixir` instead of `elixirc`, the module was compiled and loaded into memory, but no `.beam` file was written to disk.
You can also load the file within `iex` by running:
Elixir projects are usually organized into three directories:
```console
$ iex math.exs
```
* `_build` - contains compilation artifacts
* `lib` - contains Elixir code (usually `.ex` files)
* `test` - contains tests (usually `.exs` files)
When working on actual projects, the build tool called `mix` will be responsible for compiling and setting up the proper paths for you. For learning and convenience purposes, we recommend you to write the following code into script files and execute them as shown above.
And then have direct access to the `Math` module.
## Function definition
@@ -194,4 +167,78 @@ IO.puts(Concat.join("Hello", "world", "_")) #=> Hello_world
When a variable is not used by a function or a clause, we add a leading underscore (`_`) to its name to signal this intent. This rule is also covered in our [Naming Conventions](../references/naming-conventions.md#underscore-_foo) document.
This finishes our short introduction to modules. In the next chapters, we will learn how to use function definitions for recursion and later on explore more functionality related to modules.
## Understanding Aliases
An alias in Elixir is a capitalized identifier (like `String`, `Keyword`, etc) which is converted to an atom during compilation. For instance, the `String` alias translates by default to the atom `:"Elixir.String"`:
```elixir
iex> is_atom(String)
true
iex> to_string(String)
"Elixir.String"
iex> :"Elixir.String" == String
true
```
By using the `alias/2` directive, we are changing the atom the alias expands to.
Aliases expand to atoms because in the Erlang Virtual Machine (and consequently Elixir) modules are always represented by atoms. By namespacing
those atoms elixir modules avoid conflicting with existing erlang modules.
```elixir
iex> List.flatten([1, [2], 3])
[1, 2, 3]
iex> :"Elixir.List".flatten([1, [2], 3])
[1, 2, 3]
```
That's the mechanism we use to call Erlang modules:
```elixir
iex> :lists.flatten([1, [2], 3])
[1, 2, 3]
```
## Module nesting
Now that we have talked about aliases, we can talk about nesting and how it works in Elixir. Consider the following example:
```elixir
defmodule Foo do
defmodule Bar do
end
end
```
The example above will define two modules: `Foo` and `Foo.Bar`. The second can be accessed as `Bar` inside `Foo` as long as they are in the same lexical scope.
If, later, the `Bar` module is moved outside the `Foo` module definition, it must be referenced by its full name (`Foo.Bar`) or an alias must be set using the `alias` directive discussed above.
**Note**: in Elixir, you don't have to define the `Foo` module before being able to define the `Foo.Bar` module, as they are effectively independent. The above could also be written as:
```elixir
defmodule Foo.Bar do
end
defmodule Foo do
alias Foo.Bar
# Can still access it as `Bar`
end
```
Aliasing a nested module does not bring parent modules into scope. Consider the following example:
```elixir
defmodule Foo do
defmodule Bar do
defmodule Baz do
end
end
end
alias Foo.Bar.Baz
# The module `Foo.Bar.Baz` is now available as `Baz`
# However, the module `Foo.Bar` is *not* available as `Bar`
```
As we will see in later chapters, aliases also play a crucial role in macros, to guarantee they are hygienic.
@@ -113,6 +113,38 @@ iex> [0 | list]
[0, 1, 2, 3]
```
In some cases, you don't care about a particular value in a pattern. It is a common practice to bind those values to the underscore, `_`. For example, if only the head of the list matters to us, we can assign the tail to underscore:
```elixir
iex> [head | _] = [1, 2, 3]
[1, 2, 3]
iex> head
1
```
The variable `_` is special in that it can never be read from. Trying to read from it gives a compile error:
```elixir
iex> _
** (CompileError) iex:1: invalid use of _. "_" represents a value to be ignored in a pattern and cannot be used in expressions
```
If a variable is mentioned more than once in a pattern, all references must bind to the same value:
```elixir
iex> {x, x} = {1, 1}
{1, 1}
iex> {x, x} = {1, 2}
** (MatchError) no match of right hand side value: {1, 2}
```
Although pattern matching allows us to build powerful constructs, its usage is limited. For instance, you cannot make function calls on the left side of a match. The following example is invalid:
```elixir
iex> length([1, [2], 3]) = 3
** (CompileError) iex:1: cannot invoke remote function :erlang.length/1 inside match
```
Pattern matching allows developers to easily destructure data types such as tuples and lists. As we will see in the following chapters, it is one of the foundations of recursion in Elixir and applies to other types as well, like maps and binaries.
## The pin operator
@@ -168,36 +200,4 @@ iex> {y, 1} = {2, 2}
** (MatchError) no match of right hand side value: {2, 2}
```
If a variable is mentioned more than once in a pattern, all references must bind to the same value:
```elixir
iex> {x, x} = {1, 1}
{1, 1}
iex> {x, x} = {1, 2}
** (MatchError) no match of right hand side value: {1, 2}
```
In some cases, you don't care about a particular value in a pattern. It is a common practice to bind those values to the underscore, `_`. For example, if only the head of the list matters to us, we can assign the tail to underscore:
```elixir
iex> [head | _] = [1, 2, 3]
[1, 2, 3]
iex> head
1
```
The variable `_` is special in that it can never be read from. Trying to read from it gives a compile error:
```elixir
iex> _
** (CompileError) iex:1: invalid use of _. "_" represents a value to be ignored in a pattern and cannot be used in expressions
```
Although pattern matching allows us to build powerful constructs, its usage is limited. For instance, you cannot make function calls on the left side of a match. The following example is invalid:
```elixir
iex> length([1, [2], 3]) = 3
** (CompileError) iex:1: cannot invoke remote function :erlang.length/1 inside match
```
This finishes our introduction to pattern matching. As we will see in the next chapter, pattern matching is very common in many language constructs and they can be further augmented with guards.
+23 -1
View File
@@ -76,6 +76,28 @@ iex> %User{} = %{}
For more details on creating, updating, and pattern matching structs, see the documentation for `%/2`.
## Dynamic struct updates
When you need to update structs with data from keyword lists or maps, use `Kernel.struct!/2`:
```elixir
iex> john = %User{name: "John", age: 27}
%User{age: 27, name: "John"}
iex> updates = [name: "Jane", age: 30]
[name: "Jane", age: 30]
iex> struct!(john, updates)
%User{age: 27, name: "Jane"}
```
`struct!/2` will raise an error if you try to set invalid fields:
```elixir
iex> struct!(john, invalid: "field")
** (KeyError) key :invalid not found in: %User{age: 27, name: "John"}
```
Use the map update syntax (`%{john | name: "Jane"}`) when you know the exact fields at compile time. Always use `struct!/2` instead of `Map` functions to preserve struct integrity.
## Structs are bare maps underneath
Structs are simply maps with a "special" field named `__struct__` that holds the name of the struct:
@@ -113,7 +135,7 @@ iex> %Product{}
%Product{name: nil}
```
You can define a structure combining both fields with explicit default values, and implicit `nil` values. In this case you must first specify the fields which implicitly default to nil:
You can define a structure combining both fields with explicit default values, and implicit `nil` values. In this case you must first specify the fields which implicitly default to `nil`:
```elixir
iex> defmodule User do
+31 -38
View File
@@ -3,7 +3,7 @@
SPDX-FileCopyrightText: 2021 The Elixir Team
-->
# Simple state management with agents
# Simple state with agents
In this chapter, we will learn how to keep and share state between multiple entities. If you have previous programming experience, you may think of globally shared variables, but the model we will learn here is quite different. The next chapters will generalize the concepts introduced here.
@@ -11,19 +11,14 @@ If you have skipped the *Getting Started* guide or read it long ago, be sure to
## The trouble with (mutable) state
Elixir is an immutable language where nothing is shared by default. If we want to share information, which can be read and modified from multiple places, we have two main options in Elixir:
Elixir is an immutable language where nothing is shared by default. If we want to share information, this is typically done by sending messages between processes.
* Using processes and message passing
* [ETS (Erlang Term Storage)](`:ets`)
We covered processes in the *Getting Started* guide. ETS (Erlang Term Storage) is a new topic that we will explore in later chapters. When it comes to processes though, we rarely hand-roll our own, instead we use the abstractions available in Elixir and OTP:
When it comes to processes though, we rarely hand-roll our own, instead we use the abstractions available in Elixir and OTP:
* `Agent` — Simple wrappers around state.
* `GenServer` — "Generic servers" (processes) that encapsulate state, provide sync and async calls, support code reloading, and more.
* `Task` — Asynchronous units of computation that allow spawning a process and potentially retrieving its result at a later time.
We will explore these abstractions as we move forward. Keep in mind that they are all implemented on top of processes using the basic features provided by the VM, like `send/2`, `receive/1`, `spawn/1` and `Process.link/1`.
Here, we will use agents, and create a module named `KV.Bucket`, responsible for storing our key-value entries in a way that allows them to be read and modified by other processes.
## Agents 101
@@ -47,7 +42,7 @@ iex> Agent.stop(agent)
:ok
```
We started an agent with an initial state of an empty list. We updated the agent's state, adding our new item to the head of the list. The second argument of `Agent.update/3` is a function that takes the agent's current state as input and returns its desired new state. Finally, we retrieved the whole list. The second argument of `Agent.get/3` is a function that takes the state as input and returns the value that `Agent.get/3` itself will return. Once we are done with the agent, we can call `Agent.stop/3` to terminate the agent process.
We started an agent with an initial state of an empty list. The `start_link/1` function returned the `:ok` tuple with a process identifier (PID) of the agent. We will use this PID for all further interactions. We then updated the agent's state, adding our new item to the head of the list. The second argument of `Agent.update/3` is a function that takes the agent's current state as input and returns its desired new state. Finally, we retrieved the whole list. The second argument of `Agent.get/3` is a function that takes the state as input and returns the value that `Agent.get/3` itself will return. Once we are done with the agent, we can call `Agent.stop/3` to terminate the agent process.
The `Agent.update/3` function accepts as a second argument any function that receives one argument and returns a value:
@@ -93,7 +88,9 @@ Also note the `async: true` option passed to `ExUnit.Case`. This option makes th
Async or not, our new test should obviously fail, as none of the functionality is implemented in the module being tested:
```text
** (UndefinedFunctionError) function KV.Bucket.start_link/1 is undefined (module KV.Bucket is not available)
1) test stores values by key (KV.BucketTest)
test/kv/bucket_test.exs:4
** (UndefinedFunctionError) function KV.Bucket.start_link/1 is undefined (module KV.Bucket is not available)
```
In order to fix the failing test, let's create a file at `lib/kv/bucket.ex` with the contents below. Feel free to give a try at implementing the `KV.Bucket` module yourself using agents before peeking at the implementation below.
@@ -104,9 +101,11 @@ defmodule KV.Bucket do
@doc """
Starts a new bucket.
All options are forwarded to `Agent.start_link/2`.
"""
def start_link(_opts) do
Agent.start_link(fn -> %{} end)
def start_link(opts) do
Agent.start_link(fn -> %{} end, opts)
end
@doc """
@@ -125,49 +124,43 @@ defmodule KV.Bucket do
end
```
The first step in our implementation is to call `use Agent`. Most of the functionality we will learn, such as `GenServer` and `Supervisor`, follow this pattern. For all of them, calling `use` generates a `child_spec/1` function with default configuration, which will be handy when we start supervising processes in chapter 4.
The first step in our implementation is to call `use Agent`. This is a pattern we will see throughout the guides and understand in depth in the next chapter.
Then we define a `start_link/1` function, which will effectively start the agent. It is a convention to define a `start_link/1` function that always accepts a list of options. We don't plan on using any options right now, but we might later on. We then proceed to call `Agent.start_link/1`, which receives an anonymous function that returns the Agent's initial state.
Then we define a `start_link/1` function, which will effectively start the agent. It is a convention to define a `start_link/1` function that always accepts a list of options. We then call `Agent.start_link/2` passing an anonymous function that returns the Agent's initial state and the same list of options we received.
We are keeping a map inside the agent to store our keys and values. Getting and putting values on the map is done with the Agent API and the capture operator `&`, introduced in [the Getting Started guide](../getting-started/anonymous-functions.md#the-capture-operator). The agent passes its state to the anonymous function via the `&1` argument when `Agent.get/2` and `Agent.update/2` are called.
Now that the `KV.Bucket` module has been defined, our test should pass! You can try it yourself by running: `mix test`.
## Test setup with ExUnit callbacks
## Naming processes
Before moving on and adding more features to `KV.Bucket`, let's talk about ExUnit callbacks. As you may expect, all `KV.Bucket` tests will require a bucket agent to be up and running. Luckily, ExUnit supports callbacks that allow us to skip such repetitive tasks.
When starting `KV.Bucket`, we pass a list of options which we forward to `Agent.start_link/2`. One of the options accepted by `Agent.start_link/2` is a name option which allows us to name a process, so we can interact with it using its name instead of its PID.
Let's rewrite the test case to use callbacks:
Let's write a test as an example. Back on `KV.BucketTest`, add this:
```elixir
defmodule KV.BucketTest do
use ExUnit.Case, async: true
test "stores values by key on a named process" do
{:ok, _} = KV.Bucket.start_link(name: :shopping_list)
assert KV.Bucket.get(:shopping_list, "milk") == nil
setup do
{:ok, bucket} = KV.Bucket.start_link([])
%{bucket: bucket}
KV.Bucket.put(:shopping_list, "milk", 3)
assert KV.Bucket.get(:shopping_list, "milk") == 3
end
test "stores values by key", %{bucket: bucket} do
assert KV.Bucket.get(bucket, "milk") == nil
KV.Bucket.put(bucket, "milk", 3)
assert KV.Bucket.get(bucket, "milk") == 3
end
end
```
We have first defined a setup callback with the help of the `setup/1` macro. The `setup/1` macro defines a callback that is run before every test, in the same process as the test itself.
Note that we need a mechanism to pass the `bucket` PID from the callback to the test. We do so by using the *test context*. When we return `%{bucket: bucket}` from the callback, ExUnit will merge this map into the test context. Since the test context is a map itself, we can pattern match the bucket out of it, providing access to the bucket inside the test:
However, keep in mind that names are shared in the current node. If two tests attempt to create two processes named `:shopping_list` at the same time, one would succeed and the other would fail. For this reason, it is a common practice in Elixir to name processes started during tests after the test itself, like this:
```elixir
test "stores values by key", %{bucket: bucket} do
# `bucket` is now the bucket from the setup block
end
test "stores values by key on a named process", config do
{:ok, _} = KV.Bucket.start_link(name: config.test)
assert KV.Bucket.get(config.test, "milk") == nil
KV.Bucket.put(config.test, "milk", 3)
assert KV.Bucket.get(config.test, "milk") == 3
end
```
You can read more about ExUnit cases in the [`ExUnit.Case` module documentation](`ExUnit.Case`) and more about callbacks in `ExUnit.Callbacks`.
The `config` argument, passed after the test name, is the *test context* and it includes configuration and metadata about the current test, which is useful in scenarios like these.
## Other agent actions
@@ -214,4 +207,4 @@ end
When a long action is performed on the server, all other requests to that particular server will wait until the action is done, which may cause some clients to timeout.
In the next chapter, we will explore GenServers, where the segregation between clients and servers is made more apparent.
Some APIs, such as GenServers, make a clearer distinction between client and server, and we will explore them in future chapters. Next let's talk about naming things, applications, and supervisors.
@@ -0,0 +1,305 @@
<!--
SPDX-License-Identifier: Apache-2.0
SPDX-FileCopyrightText: 2021 The Elixir Team
-->
# Configuration and distribution
So far we have hardcoded our applications to run a web server on port 4040. This has been somewhat problematic since we can't, for example, run our development server and tests at the same time. In this chapter, we will learn how to use the application environment for configuration, paving the way for us to enable distribution by running multiple development servers on the same machine (on different ports).
In this last guide, we will make the routing table for our distributed key-value store configurable, and then finally package the software for production.
Let's do this.
## Application environment
In the chapter [Registries, applications, and supervisors](supervisor-and-application.md), we have learned that our project is backed by an application, which bundles our modules and specifies how your supervision tree starts and shuts down. Each application can also have its own configuration, which in Erlang/OTP (and therefore Elixir) is called "application environment".
We can use the application environment to configure our own application, as well as others. Let's see the application environment in practice. Create a file `config/runtime.exs` with the following:
```elixir
import Config
port =
cond do
port_env = System.get_env("PORT") ->
String.to_integer(port_env)
config_env() == :test ->
4040
true ->
4050
end
config :kv, :port, port
```
The above is attempting to read the "PORT" environment variable and use it as the port if defined. Otherwise, we default to port `4040` for tests and port `4050` for other environments, eliminating the conflict between environments we have seen in the past. Then we store its value under the `:port` key of our `:kv` application.
Now we just need to read this configuration. Open up `lib/kv.ex` and the `start/2` function to the following:
```elixir
def start(_type, _args) do
port = Application.fetch_env!(:kv, :port)
children = [
{Registry, name: KV, keys: :unique},
{DynamicSupervisor, name: KV.BucketSupervisor, strategy: :one_for_one},
{Task.Supervisor, name: KV.ServerSupervisor},
Supervisor.child_spec({Task, fn -> KV.Server.accept(port) end}, restart: :permanent)
]
Supervisor.start_link(children, strategy: :one_for_one)
end
```
Run `iex -S mix` and you will see the following message printed:
```text
[info] Accepting connections on port 4050
```
Run tests, without killing the development server, and you will see it running on port 4040.
Our change was straight-forward. We used `Application.fetch_env!/2` to read the entry for `port` in `:kv`'s environment. We explicitly used `fetch_env!/2` (instead of `get_env/2` or `fetch_env`) because it will raise if the port was not configured (preventing the app from booting).
## Compile vs runtime configuration
Configuration files provide a mechanism for us to configure the environment of any application. Elixir provides two configuration entry points:
* `config/config.exs` — this file is read at build time, before we compile our application and before we even load our dependencies. This means we can't access the code in our application nor in our dependencies. However, it means we can control how they are compiled
* `config/runtime.exs` — this file is read after our application and dependencies are compiled and therefore it can configure how our application works at runtime. If you want to read system environment variables (via `System.get_env/1`) or access external configuration, this is the appropriate place to do so
You can learn more about configuration in the `Config` and `Config.Provider` modules.
Generally speaking, we use `Application.fetch_env!/2` (and friends) to read runtime configuration. `Application.compile_env/2` is available for reading compile-time configuration. This allows Elixir to track which modules to recompile when the compilation environment changes.
Now that we can start multiple servers, let's explore distribution.
## Our first distributed code
Elixir ships with facilities to connect nodes and exchange information between them. In fact, we use the same concepts of processes, message passing and receiving messages when working in a distributed environment because Elixir processes are *location transparent*. This means that when sending a message, it doesn't matter if the recipient process is on the same node or on another node, the VM will be able to deliver the message in both cases.
In order to run distributed code, we need to start the VM with a name. The name can be short (when in the same network) or long (requires the full computer address). Let's start a new IEx session:
```console
$ iex --sname foo
```
You can see now the prompt is slightly different and shows the node name followed by the computer name:
Interactive Elixir - press Ctrl+C to exit (type h() ENTER for help)
iex(foo@jv)1>
My computer is named `jv`, so I see `foo@jv` in the example above, but you will get a different result. We will use `foo@computer-name` in the following examples and you should update them accordingly when trying out the code.
Let's define a module named `Hello` in this shell:
```elixir
iex> defmodule Hello do
...> def world, do: IO.puts("hello world")
...> end
```
If you have another computer on the same network with both Erlang and Elixir installed, you can start another shell on it. If you don't, you can start another IEx session in another terminal. In either case, give it the short name of `bar`:
```console
$ iex --sname bar
```
Note that inside this new IEx session, we cannot access `Hello.world/0`:
```elixir
iex> Hello.world
** (UndefinedFunctionError) function Hello.world/0 is undefined (module Hello is not available)
Hello.world()
```
However, we can spawn a new process on `foo@computer-name` from `bar@computer-name`! Let's give it a try (where `@computer-name` is the one you see locally):
```elixir
iex> Node.spawn_link(:"foo@computer-name", fn -> Hello.world() end)
#PID<9014.59.0>
hello world
```
Elixir spawned a process on another node and returned its PID. You can see the PID number no longer starts with zero, showing it belongs to another node. The code then executed on the other node where the `Hello.world/0` function exists and invoked that function. Note that the result of "hello world" was printed on the current node `bar` and not on `foo`. In other words, the message to be printed was sent back from `foo` to `bar`. This happens because the process spawned on the other node (`foo`) knows all the output should be sent back to the original node!
We can send and receive messages from the PID returned by `Node.spawn_link/2` as usual. Let's try a quick ping-pong example:
```elixir
iex> pid = Node.spawn_link(:"foo@computer-name", fn ->
...> receive do
...> {:ping, client} -> send(client, :pong)
...> end
...> end)
#PID<9014.59.0>
iex> send(pid, {:ping, self()})
{:ping, #PID<0.73.0>}
iex> flush()
:pong
:ok
```
In other words, we can spawn processes in other nodes, hold onto their PIDs, and then send messages to them as if they were running on the same machine. That's the *location transparency* principle. And because everything we have built so far was built on top of messaging passing, we should be able to adjust our key-value store to become a distributed one with little work.
## Distributed naming registry with `:global`
First, let's check that our code is not currently distributed. Start a new node like this:
```console
$ PORT=4100 iex --sname foo -S mix
```
And the other like this:
```console
$ PORT=4101 iex --sname bar -S mix
```
Now, within `foo@computer-name`, do this:
```elixir
iex> :erpc.call(:"bar@computer-name", KV, :create_bucket, ["shopping"])
{:ok, #PID<22121.164.0>}
```
Instead of using `Node.spawn_link/2`, we used [Erlang's builtin RPC module](`:erpc`) to call the function `create_bucket` in the `KV` module passing a one element list with the string "shopping" as the argument list. We could have used `Node.spawn_link/2`, but `:erpc.call/4` conveniently returns the result of the invocation.
Still in `foo@computer-name`, let's try to access the bucket:
```elixir
iex> KV.lookup_bucket("shopping")
nil
```
It returns `nil`. However, if you run `KV.lookup_bucket("shopping")` in `bar@computer-name`, it will return the proper bucket. In other words, the nodes can communicate with each other, but buckets spawned in one node are not visible to the other.
This is because we are using [Elixir's Registry](`Registry`) to name our buckets, which is a **local** process registry. In other words, it is designed for processes running on a single node and not for distribution.
Luckily, Erlang ships with a distributed registry called [`:global`](`:global`), which is directly supported by the `:name` option by passing a `{:global, name}` tuple. All we need to do is update the `via/1` function in `lib/kv.ex` from this:
```elixir
defp via(name), do: {:via, Registry, {KV, name}}
```
to this:
```elixir
defp via(name), do: {:global, name}
```
Do the change above and restart both `foo@computer-name` and `bar@computer-name`. Now, back on `foo@computer-name`, let's give it another try:
```elixir
iex> :erpc.call(:"bar@computer-name", KV, :create_bucket, ["shopping"])
{:ok, #PID<21821.179.0>}
iex> KV.lookup_bucket("shopping")
#PID<21821.179.0>
```
And there you go! By simply changing which naming registry we used, we now have a distributed key value store. You can even try using `telnet` to connect to the servers on different ports and validate that changes in one session are visible in the other one. Exciting!
## Node discovery and dependencies
There is one essential ingredient to wrap up our distributed key-value store. In order for the `:global` registry to work, we need to make sure the nodes are connected to each other. When we run `:erpc` call passing the node name:
```elixir
:erpc.call(:"bar@computer-name", KV, :create_bucket, ["shopping"])
```
Elixir automatically connected the nodes together. This is easy to do in an IEx session when both instances are running on the same machine but it requires more work in a production environment, where instances are on different machines which may be started at any time and running on different IP addresses.
Luckily for us, this is also a well-solved problem. For example, if you are using [the Phoenix web framework](https://phoenixframework.org) in production, it ships with [the `dns_cluster` package](https://github.com/phoenixframework/dns_cluster), which automatically runs DNS queries to find new nodes and connect them. If you are using Kubernetes or cloud providers, [packages like `libcluster`](https://github.com/bitwalker/libcluster) ship with different strategies to discover and connect nodes.
Installing dependencies in Elixir is simple. Most commonly, we use the [Hex Package Manager](https://hex.pm), by listing the dependency inside the deps function in our `mix.exs` file:
```elixir
def deps do
[{:dns_cluster, "~> 0.2"}]
end
```
This dependency refers to the latest version of `dns_cluster` in the 0.x version series that has been pushed to Hex. This is indicated by the `~>` preceding the version number. For more information on specifying version requirements, see the documentation for the `Version` module.
Typically, stable releases are pushed to Hex. If you want to depend on an external dependency still in development, Mix is able to manage Git dependencies too:
```elixir
def deps do
[{:dns_cluster, git: "https://github.com/phoenixframework/dns_cluster.git"}]
end
```
You will notice that when you add a dependency to your project, Mix generates a `mix.lock` file that guarantees *repeatable builds*. The lock file must be checked in to your version control system, to guarantee that everyone who uses the project will use the same dependency versions as you.
Mix provides many tasks for working with dependencies, which can be seen in `mix help`:
```console
$ mix help
mix deps # Lists dependencies and their status
mix deps.clean # Deletes the given dependencies' files
mix deps.compile # Compiles dependencies
mix deps.get # Gets all out of date dependencies
mix deps.tree # Prints the dependency tree
mix deps.unlock # Unlocks the given dependencies
mix deps.update # Updates the given dependencies
```
The most common tasks are `mix deps.get` and `mix deps.update`. Once fetched, dependencies are automatically compiled for you. You can read more about deps by running `mix help deps`.
To wrap up this chapter, we will build a very simple node discovery mechanism, where the name of the nodes we should connect to are given on boot, using the lessons we learned in this chapter.
## `Node.connect/1`
We will change our application to support a "NODES" environment variable with the name of all nodes each instance should connect to.
Open up `config/runtime.exs` and add this to the bottom:
```elixir
nodes =
System.get_env("NODES", "")
|> String.split(",", trim: true)
|> Enum.map(&String.to_atom/1)
config :kv, :nodes, nodes
```
We fetch the environment variable, split it on "," while discarding all empty strings, and then convert each entry to an atom, as node names are atoms.
Now, in your `start/2` callback, we will add this to of the `start/2` function:
```elixir
def start(_type, _args) do
for node <- Application.fetch_env!(:kv, :nodes) do
Node.connect(node)
end
```
Now we can start our nodes as:
```console
$ NODES="foo@computer-name,bar@computer-name" PORT=4040 iex --sname foo -S mix
$ NODES="foo@computer-name,bar@computer-name" PORT=4041 iex --sname bar -S mix
```
And they should connect to each other. Give it a try!
In an actual production system, there is some additional care we must take. For example, we often use `--name` instead of `--sname` and give fully qualified node names.
Furthermore, when connecting two instances, we must guarantee they have the same cookie, which is a secret Erlang uses to authorize the connection. When they run on the same machine, they share the same cookie by default, but it must be either explicitly set or shared in other ways when deploying in a cluster.
We will revisit these topics in the last chapter when we talk about releases.
## Distributed system trade-offs
In this chapter, we made our key-value store distributed by using the `:global` naming registry. However, it is important to keep in mind that every distributed system, be it a library or a full-blown database, is designed with a series of trade-offs in mind.
In particular, `:global` requires consistency across all known nodes whenever a new bucket is created. For example, if your cluster has three nodes, creating a new bucket will require all three nodes to agree on its name. This means if one node is unresponsive, perhaps due to a [network partition](https://en.wikipedia.org/wiki/Network_partition), the node will have to either reconnect or be kicked out before registration succeeds. This also means that, as your cluster grows in size, registration becomes more expensive, although lookups are always cheap and immediate. Within the ecosystem, there are other named registries, which explore different trade-offs, such as [Syn](https://github.com/ostinelli/syn).
Further complications arise when we consider storage. Today, when our nodes terminate, we lose all data stored in the buckets. In our current design, since we allow each node to store their own buckets, it means we would need to backup each node. And, if we don't want data losses, we would also need to replicate the data.
For those reasons, it is still very common to use a database (or any storage system) when writing production applications in Elixir, and use Elixir to implement the realtime and collaborative aspects of your applications that extend beyond storage. For example, we can use Elixir to track which clients are connected to the cluster at any given moment or implement a feed where users are notified in realtime whenever items are added or removed from a bucket.
In fact, that's exactly what we will build in the next chapter. Allowing us to wrap up everything we have learned so far and also talk about one of the essential building blocks in Elixir software: GenServers.
@@ -1,429 +0,0 @@
<!--
SPDX-License-Identifier: Apache-2.0
SPDX-FileCopyrightText: 2021 The Elixir Team
-->
# Configuration and releases
In this last guide, we will make the routing table for our distributed key-value store configurable, and then finally package the software for production.
Let's do this.
## Application environment
So far we have hard-coded the routing table into the `KV.Router` module. However, we would like to make the table dynamic. This allows us not only to configure development/test/production, but also to allow different nodes to run with different entries in the routing table. There is a feature of OTP that does exactly that: the application environment.
Each application has an environment that stores the application's specific configuration by key. For example, we could store the routing table in the `:kv` application environment, giving it a default value and allowing other applications to change the table as needed.
Open up `apps/kv/mix.exs` and change the `application/0` function to return the following:
```elixir
def application do
[
extra_applications: [:logger],
env: [routing_table: []],
mod: {KV, []}
]
end
```
We have added a new `:env` key to the application. It returns the application default environment, which has an entry of key `:routing_table` and value of an empty list. It makes sense for the application environment to ship with an empty table, as the specific routing table depends on the testing/deployment structure.
In order to use the application environment in our code, we need to replace `KV.Router.table/0` with the definition below:
```elixir
@doc """
The routing table.
"""
def table do
Application.fetch_env!(:kv, :routing_table)
end
```
We use `Application.fetch_env!/2` to read the entry for `:routing_table` in `:kv`'s environment. You can find more information and other functions to manipulate the app environment in the `Application` module.
Since our routing table is now empty, our distributed tests should fail. Restart the apps and re-run tests to see the failure:
```console
$ iex --sname bar -S mix
$ elixir --sname foo -S mix test --only distributed
```
We need a way to configure the application environment. That's when we use configuration files.
## Configuration
Configuration files provide a mechanism for us to configure the environment of any application. Elixir provides two configuration entry points:
* `config/config.exs` — this file is read at build time, before we compile our application and before we even load our dependencies. This means we can't access the code in our application nor in our dependencies. However, it means we can control how they are compiled
* `config/runtime.exs` — this file is read after our application and dependencies are compiled and therefore it can configure how our application works at runtime. If you want to read system environment variables (via `System.get_env/1`) or access external configuration, this is the appropriate place to do so
You can learn more about configuration in the `Config` and `Config.Provider` modules. For now, let's see an example.
We can configure IEx default prompt to another value by creating a `config/runtime.exs` file with the following content:
```elixir
import Config
config :iex, default_prompt: ">>>"
```
Start IEx with `iex -S mix` and you can see that the IEx prompt has changed.
This means we can also configure our `:routing_table` directly in the `config/runtime.exs` file. However, which configuration value should we use?
Currently we have two tests tagged with `@tag :distributed`. The "server interaction" test in `KVServerTest`, and the "route requests across nodes" in `KV.RouterTest`. Both tests are failing since they require a routing table, which is currently empty.
For simplicity, we will define a routing table that always points to the current node. That's the table we will use for development and most of our tests. Back in `config/runtime.exs`, add this line:
```elixir
config :kv, :routing_table, [{?a..?z, node()}]
```
With such a simple table available, we can now remove `@tag :distributed` from the test in `test/kv_server_test.exs`. If you run the complete suite, the test should now pass.
However, for the tests in `KV.RouterTest`, we effectively need two nodes in our routing table. To do so, we will write a setup block that runs before all tests in that file. The setup block will change the application environment and revert it back once we are done, like this:
```elixir
defmodule KV.RouterTest do
use ExUnit.Case
setup_all do
current = Application.get_env(:kv, :routing_table)
Application.put_env(:kv, :routing_table, [
{?a..?m, :"foo@computer-name"},
{?n..?z, :"bar@computer-name"}
])
on_exit fn -> Application.put_env(:kv, :routing_table, current) end
end
@tag :distributed
test "route requests across nodes" do
```
Note we removed `async: true` from `use ExUnit.Case`. Since the application environment is a global storage, tests that modify it cannot run concurrently. With all changes in place, all tests should pass, including the distributed one.
## Releases
Now that our application runs distributed, you may be wondering how we can package our application to run in production. After all, all of our code so far depends on Erlang and Elixir versions that are installed in your current system. To achieve this goal, Elixir provides releases.
A release is a self-contained directory that consists of your application code, all of its dependencies, plus the whole Erlang Virtual Machine (VM) and runtime. Once a release is assembled, it can be packaged and deployed to a target as long as the target runs on the same operating system (OS) distribution and version as the machine that assembled the release.
In a regular project, we can assemble a release by simply running `mix release`. However, we have an umbrella project, and in such cases Elixir requires some extra input from us. Let's see what is necessary:
```shell
$ MIX_ENV=prod mix release
** (Mix) Umbrella projects require releases to be explicitly defined with a non-empty applications key that chooses which umbrella children should be part of the releases:
releases: [
foo: [
applications: [child_app_foo: :permanent]
],
bar: [
applications: [child_app_bar: :permanent]
]
]
Alternatively you can perform the release from the children applications
```
That's because an umbrella project gives us plenty of options when deploying the software. We can:
* deploy all applications in the umbrella to a node that will work as both TCP server and key-value storage
* deploy the `:kv_server` application to work only as a TCP server as long as the routing table points only to other nodes
* deploy only the `:kv` application when we want a node to work only as storage (no TCP access)
As a starting point, let's define a release that includes both `:kv_server` and `:kv` applications. We will also add a version to it. Open up the `mix.exs` in the umbrella root and add inside `def project`:
```elixir
releases: [
foo: [
version: "0.0.1",
applications: [kv_server: :permanent, kv: :permanent]
]
]
```
That defines a release named `foo` with both `kv_server` and `kv` applications. Their mode is set to `:permanent`, which means that, if those applications crash, the whole node terminates. That's reasonable since those applications are essential to our system.
Before we assemble the release, let's also define our routing table for production. Given we expect to have two nodes, we need to update `config/runtime.exs` to look like this:
```elixir
import Config
config :kv, :routing_table, [{?a..?z, node()}]
if config_env() == :prod do
config :kv, :routing_table, [
{?a..?m, :"foo@computer-name"},
{?n..?z, :"bar@computer-name"}
]
end
```
We have hard-coded the table and node names, which is good enough for our example, but you would likely move it to an external configuration system in an actual production setup. We have also wrapped it in a `config_env() == :prod` check, so this configuration does not apply to other environments.
With the configuration in place, let's give assembling the release another try:
$ MIX_ENV=prod mix release foo
* assembling foo-0.0.1 on MIX_ENV=prod
* skipping runtime configuration (config/runtime.exs not found)
Release created at _build/prod/rel/foo!
# To start your system
_build/prod/rel/foo/bin/foo start
Once the release is running:
# To connect to it remotely
_build/prod/rel/foo/bin/foo remote
# To stop it gracefully (you may also send SIGINT/SIGTERM)
_build/prod/rel/foo/bin/foo stop
To list all commands:
_build/prod/rel/foo/bin/foo
Excellent! A release was assembled in `_build/prod/rel/foo`. Inside the release, there will be a `bin/foo` file which is the entry point to your system. It supports multiple commands, such as:
* `bin/foo start`, `bin/foo start_iex`, `bin/foo restart`, and `bin/foo stop` — for general management of the release
* `bin/foo rpc COMMAND` and `bin/foo remote` — for running commands on the running system or to connect to the running system
* `bin/foo eval COMMAND` — to start a fresh system that runs a single command and then shuts down
* `bin/foo daemon` and `bin/foo daemon_iex` — to start the system as a daemon on Unix-like systems
* `bin/foo install` — to install the system as a service on Windows machines
If you run `bin/foo start`, it will start the system using a short name (`--sname`) equal to the release name, which in this case is `foo`. The next step is to start a system named `bar`, so we can connect `foo` and `bar` together, like we did in the previous chapter. But before we achieve this, let's talk a bit about the benefits of releases.
## Why releases?
Releases allow developers to precompile and package all of their code and the runtime into a single unit. The benefits of releases are:
* Code preloading. The VM has two mechanisms for loading code: interactive and embedded. By default, it runs in the interactive mode which dynamically loads modules when they are used for the first time. The first time your application calls `Enum.map/2`, the VM will find the `Enum` module and load it. There's a downside. When you start a new server in production, it may need to load many other modules, causing the first requests to have an unusual spike in response time. Releases run in embedded mode, which loads all available modules upfront, guaranteeing your system is ready to handle requests after booting.
* Configuration and customization. Releases give developers fine grained control over system configuration and the VM flags used to start the system.
* Self-contained. A release does not require the source code to be included in your production artifacts. All of the code is precompiled and packaged. Releases do not even require Erlang or Elixir on your servers, as they include the Erlang VM and its runtime by default. Furthermore, both Erlang and Elixir standard libraries are stripped to bring only the parts you are actually using.
* Multiple releases. You can assemble different releases with different configuration per application or even with different applications altogether.
We have written extensive documentation on releases, so [please check the official documentation for more information](`mix release`). For now, we will continue exploring some of the features outlined above.
## Assembling multiple releases
So far, we have assembled a release named `foo`, but our routing table contains information for both `foo` and `bar`. Let's start `foo`:
$ _build/prod/rel/foo/bin/foo start
16:58:58.508 [info] Accepting connections on port 4040
And let's connect to it and issue a request in another terminal:
$ telnet 127.0.0.1 4040
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
CREATE bitsandpieces
OK
PUT bitsandpieces sword 1
OK
GET bitsandpieces sword
1
OK
GET shopping foo
Connection closed by foreign host.
Our application works already when we operate on the bucket named "bitsandpieces". But since the "shopping" bucket would be stored on `bar`, the request fails as `bar` is not available. If you go back to the terminal running `foo`, you will see:
17:16:19.555 [error] Task #PID<0.622.0> started from #PID<0.620.0> terminating
** (stop) exited in: GenServer.call({KV.RouterTasks, :"bar@computer-name"}, {:start_task, [{:"foo@josemac-2", #PID<0.622.0>, #PID<0.622.0>}, [#PID<0.622.0>, #PID<0.620.0>, #PID<0.618.0>], :monitor, {KV.Router, :route, ["shopping", KV.Registry, :lookup, [KV.Registry, "shopping"]]}], :temporary, nil}, :infinity)
** (EXIT) no connection to bar@computer-name
(elixir) lib/gen_server.ex:1010: GenServer.call/3
(elixir) lib/task/supervisor.ex:454: Task.Supervisor.async/6
(kv) lib/kv/router.ex:21: KV.Router.route/4
(kv_server) lib/kv_server/command.ex:74: KVServer.Command.lookup/2
(kv_server) lib/kv_server.ex:29: KVServer.serve/1
(elixir) lib/task/supervised.ex:90: Task.Supervised.invoke_mfa/2
(stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
Function: #Function<0.128611034/0 in KVServer.loop_acceptor/1>
Args: []
Let's now define a release for `:bar`. One first step could be to define a release exactly like `foo` inside `mix.exs`. Additionally we will set the `cookie` option on both releases to `weknoweachother` in order for them to allow connections from each other. See the [Distributed Erlang Documentation](http://www.erlang.org/doc/reference_manual/distributed.html) for further information on this topic:
```elixir
releases: [
foo: [
version: "0.0.1",
applications: [kv_server: :permanent, kv: :permanent],
cookie: "weknoweachother"
],
bar: [
version: "0.0.1",
applications: [kv_server: :permanent, kv: :permanent],
cookie: "weknoweachother"
]
]
```
And now let's assemble both releases:
```shell
$ MIX_ENV=prod mix release foo
$ MIX_ENV=prod mix release bar
```
Stop `foo` if it's still running and re-start it to load the `cookie`:
```shell
$ _build/prod/rel/foo/bin/foo start
```
And start `bar` in another terminal:
```shell
$ _build/prod/rel/bar/bin/bar start
```
You should see an error like the error below happen 5 times, before the application finally shuts down:
```text
17:21:57.567 [error] Task #PID<0.620.0> started from KVServer.Supervisor terminating
** (MatchError) no match of right hand side value: {:error, :eaddrinuse}
(kv_server) lib/kv_server.ex:12: KVServer.accept/1
(elixir) lib/task/supervised.ex:90: Task.Supervised.invoke_mfa/2
(stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
Function: #Function<0.98032413/0 in KVServer.Application.start/2>
Args: []
```
That's happening because the release `foo` is already listening on port `4040` and `bar` is trying to do the same! One option could be to move the `:port` configuration to the application environment, like we did for the routing table, and setup different ports per node.
But let's try something else. Let's make it so the `bar` release contains only the `:kv` application. So it works as a storage but it won't have a front-end. Change the `:bar` information to this:
```elixir
releases: [
foo: [
version: "0.0.1",
applications: [kv_server: :permanent, kv: :permanent],
cookie: "weknoweachother"
],
bar: [
version: "0.0.1",
applications: [kv: :permanent],
cookie: "weknoweachother"
]
]
```
And now let's assemble `bar` once more:
$ MIX_ENV=prod mix release bar
And finally successfully boot it:
$ _build/prod/rel/bar/bin/bar start
If you connect to localhost once again and perform another request, now everything should work, as long as the routing table contains the correct node names. Outstanding!
With releases, we were able to "cut different slices" of our project and prepared them to run in production, all packaged into a single directory.
## Configuring releases
Releases also provide built-in hooks for configuring almost every need of the production system:
* `config/config.exs` — provides build-time application configuration, which is executed before our application compiles. This file often imports configuration files based on the environment, such as `config/dev.exs` and `config/prod.exs`.
* `config/runtime.exs` — provides runtime application configuration. It is executed every time the release boots and is further extensible via config providers.
* `rel/env.sh.eex` and `rel/env.bat.eex` — template files that are copied into every release and executed on every command to set up environment variables, including ones specific to the VM, and the general environment.
* `rel/vm.args.eex` — a template file that is copied into every release and provides static configuration of the Erlang Virtual Machine and other runtime flags.
As we have seen, `config/config.exs` and `config/runtime.exs` are loaded during releases and regular Mix commands. On the other hand, `rel/env.sh.eex` and `rel/vm.args.eex` are specific to releases. Let's take a look.
### Operating System environment configuration
Every release contains an environment file, named `env.sh` on Unix-like systems and `env.bat` on Windows machines, that executes before the Elixir system starts. In this file, you can execute any OS-level code, such as invoke other applications, set environment variables and so on. Some of those environment variables can even configure how the release itself runs.
For instance, releases run using short-names (`--sname`). However, if you want to actually run a distributed key-value store in production, you will need multiple nodes and start the release with the `--name` option. We can achieve this by setting the `RELEASE_DISTRIBUTION` environment variable inside the `env.sh` and `env.bat` files. Mix already has a template for said files which we can customize, so let's ask Mix to copy them to our application:
$ mix release.init
* creating rel/vm.args.eex
* creating rel/remote.vm.args.eex
* creating rel/env.sh.eex
* creating rel/env.bat.eex
If you open up `rel/env.sh.eex`, you will see:
```shell
#!/bin/sh
# # Sets and enables heart (recommended only in daemon mode)
# case $RELEASE_COMMAND in
# daemon*)
# HEART_COMMAND="$RELEASE_ROOT/bin/$RELEASE_NAME $RELEASE_COMMAND"
# export HEART_COMMAND
# export ELIXIR_ERL_OPTIONS="-heart"
# ;;
# *)
# ;;
# esac
# # Set the release to load code on demand (interactive) instead of preloading (embedded).
# export RELEASE_MODE=interactive
# # Set the release to work across nodes.
# # RELEASE_DISTRIBUTION must be "sname" (local), "name" (distributed) or "none".
# export RELEASE_DISTRIBUTION=name
# export RELEASE_NODE=<%= @release.name %>
```
The steps necessary to work across nodes is already commented out as an example. You can enable full distribution by uncommenting the last two lines by removing the leading `# `.
If you are on Windows, you will have to open up `rel/env.bat.eex`, where you will find this:
```bat
@echo off
rem Set the release to load code on demand (interactive) instead of preloading (embedded).
rem set RELEASE_MODE=interactive
rem Set the release to work across nodes.
rem RELEASE_DISTRIBUTION must be "sname" (local), "name" (distributed) or "none".
rem set RELEASE_DISTRIBUTION=name
rem set RELEASE_NODE=<%= @release.name %>
```
Once again, uncomment the last two lines by removing the leading `rem ` to enable full distribution. And that's all!
### VM arguments
The `rel/vm.args.eex` allows you to specify low-level flags that control how the Erlang VM and its runtime operate. You specify entries as if you were specifying arguments in the command line with code comments also supported. Here is the default generated file:
## Customize flags given to the VM: https://www.erlang.org/doc/man/erl.html
## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here
## Increase number of concurrent ports/sockets
##+Q 65536
## Tweak GC to run more often
##-env ERL_FULLSWEEP_AFTER 10
You can see [a complete list of VM arguments and flags in the Erlang documentation](http://www.erlang.org/doc/man/erl.html).
## Summing up
Throughout the guide, we have built a very simple distributed key-value store as an opportunity to explore many constructs like generic servers, supervisors, tasks, agents, applications and more. Not only that, we have written tests for the whole application, got familiar with ExUnit, and learned how to use the Mix build tool to accomplish a wide range of tasks.
If you are looking for a distributed key-value store to use in production, you should definitely look into [Riak](http://riak.com/products/riak-kv/), which also runs in the Erlang VM. In Riak, the buckets are replicated, to avoid data loss, and instead of a router, they use [consistent hashing](https://en.wikipedia.org/wiki/Consistent_hashing) to map a bucket to a node. A consistent hashing algorithm helps reduce the amount of data that needs to be migrated when new storage nodes are added to your live system.
Of course, Elixir can be used for much more than distributed key-value stores. Embedded systems, data-processing and data-ingestion, web applications, audio/video streaming systems, and others are many of the different domains Elixir excels at. We hope this guide has prepared you to explore any of those domains or any future domain you may desire to bring Elixir into.
Happy coding!
@@ -1,305 +0,0 @@
<!--
SPDX-License-Identifier: Apache-2.0
SPDX-FileCopyrightText: 2021 The Elixir Team
-->
# Dependencies and umbrella projects
In this chapter, we will discuss how to manage dependencies in Mix.
Our `kv` application is complete, so it's time to implement the server that will handle the requests we defined in the first chapter:
```text
CREATE shopping
OK
PUT shopping milk 1
OK
PUT shopping eggs 3
OK
GET shopping milk
1
OK
DELETE shopping eggs
OK
```
However, instead of adding more code to the `kv` application, we are going to build the TCP server as another application that is a client of the `kv` application. Since the whole runtime and Elixir ecosystem are geared towards applications, it makes sense to break our projects into smaller applications that work together rather than building a big, monolithic app.
Before creating our new application, we must discuss how Mix handles dependencies. In practice, there are two kinds of dependencies we usually work with: internal and external dependencies. Mix supports mechanisms to work with both.
## External dependencies
External dependencies are the ones not tied to your business domain. For example, if you need an HTTP API for your distributed KV application, you can use the [Plug](https://github.com/elixir-lang/plug) project as an external dependency.
Installing external dependencies is simple. Most commonly, we use the [Hex Package Manager](https://hex.pm), by listing the dependency inside the deps function in our `mix.exs` file:
```elixir
def deps do
[{:plug, "~> 1.0"}]
end
```
This dependency refers to the latest version of Plug in the 1.x.x version series that has been pushed to Hex. This is indicated by the `~>` preceding the version number. For more information on specifying version requirements, see the documentation for the `Version` module.
Typically, stable releases are pushed to Hex. If you want to depend on an external dependency still in development, Mix is able to manage Git dependencies too:
```elixir
def deps do
[{:plug, git: "https://github.com/elixir-lang/plug.git"}]
end
```
You will notice that when you add a dependency to your project, Mix generates a `mix.lock` file that guarantees *repeatable builds*. The lock file must be checked in to your version control system, to guarantee that everyone who uses the project will use the same dependency versions as you.
Mix provides many tasks for working with dependencies, which can be seen in `mix help`:
```console
$ mix help
mix deps # Lists dependencies and their status
mix deps.clean # Deletes the given dependencies' files
mix deps.compile # Compiles dependencies
mix deps.get # Gets all out of date dependencies
mix deps.tree # Prints the dependency tree
mix deps.unlock # Unlocks the given dependencies
mix deps.update # Updates the given dependencies
```
The most common tasks are `mix deps.get` and `mix deps.update`. Once fetched, dependencies are automatically compiled for you. You can read more about deps by typing `mix help deps`, and in the documentation for the `Mix.Tasks.Deps` module.
## Internal dependencies
Internal dependencies are the ones that are specific to your project. They usually don't make sense outside the scope of your project/company/organization. Most of the time, you want to keep them private, whether due to technical, economic or business reasons.
If you have an internal dependency, Mix supports two methods to work with them: Git repositories or umbrella projects.
For example, if you push the `kv` project to a Git repository, you'll need to list it in your deps code in order to use it:
```elixir
def deps do
[{:kv, git: "https://github.com/YOUR_ACCOUNT/kv.git"}]
end
```
If the repository is private though, you may need to specify the private URL `git@github.com:YOUR_ACCOUNT/kv.git`. In any case, Mix will be able to fetch it for you as long as you have the proper credentials.
Using Git repositories for internal dependencies is somewhat discouraged in Elixir. Remember that the runtime and the Elixir ecosystem already provide the concept of applications. As such, we expect you to frequently break your code into applications that can be organized logically, even within a single project.
However, if you push every application as a separate project to a Git repository, your projects may become very hard to maintain as you will spend a lot of time managing those Git repositories rather than writing your code.
For this reason, Mix supports "umbrella projects". Umbrella projects are used to build applications that run together in a single repository. That is exactly the style we are going to explore in the next sections.
Let's create a new Mix project. We are going to creatively name it `kv_umbrella`, and this new project will have both the existing `kv` application and the new `kv_server` application inside. The directory structure will look like this:
+ kv_umbrella
+ apps
+ kv
+ kv_server
The interesting thing about this approach is that Mix has many conveniences for working with such projects, such as the ability to compile and test all applications inside `apps` with a single command. However, even though they are all listed together inside `apps`, they are still decoupled from each other, so you can build, test and deploy each application in isolation if you want to.
So let's get started!
## Umbrella projects
Let's start a new project using `mix new`. This new project will be named `kv_umbrella` and we need to pass the `--umbrella` option when creating it. Do not create this new project inside the existing `kv` project!
```console
$ mix new kv_umbrella --umbrella
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating apps
* creating config
* creating config/config.exs
```
From the printed information, we can see far fewer files are generated. The generated `mix.exs` file is different too. Let's take a look (comments have been removed):
```elixir
defmodule KvUmbrella.MixProject do
use Mix.Project
def project do
[
apps_path: "apps",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
defp deps do
[]
end
end
```
What makes this project different from the previous one is the `apps_path: "apps"` entry in the project definition. This means this project will act as an umbrella. Such projects do not have source files nor tests, although they can have their own dependencies. Each child application must be defined inside the `apps` directory.
Let's move inside the apps directory and start building `kv_server`. This time, we are going to pass the `--sup` flag, which will tell Mix to generate a supervision tree automatically for us, instead of building one manually as we did in previous chapters:
```console
$ cd kv_umbrella/apps
$ mix new kv_server --module KVServer --sup
```
The generated files are similar to the ones we first generated for `kv`, with a few differences. Let's open up `mix.exs`:
```elixir
defmodule KVServer.MixProject do
use Mix.Project
def project do
[
app: :kv_server,
version: "0.1.0",
build_path: "../../_build",
config_path: "../../config/config.exs",
deps_path: "../../deps",
lockfile: "../../mix.lock",
elixir: "~> 1.14",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications
def application do
[
extra_applications: [:logger],
mod: {KVServer.Application, []}
]
end
# Run "mix help deps" to learn about dependencies
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"},
# {:sibling_app_in_umbrella, in_umbrella: true},
]
end
end
```
First of all, since we generated this project inside `kv_umbrella/apps`, Mix automatically detected the umbrella structure and added four lines to the project definition:
```elixir
build_path: "../../_build",
config_path: "../../config/config.exs",
deps_path: "../../deps",
lockfile: "../../mix.lock",
```
Those options mean all dependencies will be checked out to `kv_umbrella/deps`, and they will share the same build, config, and lock files. We haven't talked about configuration yet, but from here we can build the intuition that all configuration and dependencies are shared across all projects in an umbrella, and it is not per application.
The second change is in the `application` function inside `mix.exs`:
```elixir
def application do
[
extra_applications: [:logger],
mod: {KVServer.Application, []}
]
end
```
Because we passed the `--sup` flag, Mix automatically added `mod: {KVServer.Application, []}`, specifying that `KVServer.Application` is our application callback module. `KVServer.Application` will start our application supervision tree.
In fact, let's open up `lib/kv_server/application.ex`:
```elixir
defmodule KVServer.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false
use Application
@impl true
def start(_type, _args) do
# List all child processes to be supervised
children = [
# Starts a worker by calling: KVServer.Worker.start_link(arg)
# {KVServer.Worker, arg},
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: KVServer.Supervisor]
Supervisor.start_link(children, opts)
end
end
```
Notice that it defines the application callback function, `start/2`, and instead of defining a supervisor named `KVServer.Supervisor` that uses the `Supervisor` module, it conveniently defined the supervisor inline! You can read more about such supervisors by reading the `Supervisor` module documentation.
We can already try out our first umbrella child. We could run tests inside the `apps/kv_server` directory, but that wouldn't be much fun. Instead, go to the root of the umbrella project and run `mix test`:
```console
$ mix test
```
And it works!
Since we want `kv_server` to eventually use the functionality we defined in `kv`, we need to add `kv` as a dependency to our application.
## Dependencies within an umbrella project
Dependencies between applications in an umbrella project must still be explicitly defined and Mix makes it easy to do so. Open up `apps/kv_server/mix.exs` and change the `deps/0` function to the following:
```elixir
defp deps do
[{:kv, in_umbrella: true}]
end
```
The line above makes `:kv` available as a dependency inside `:kv_server` and automatically starts the `:kv` application before the server starts.
Finally, copy the `kv` application we have built so far to the `apps` directory in our new umbrella project. The final directory structure should match the structure we mentioned earlier:
+ kv_umbrella
+ apps
+ kv
+ kv_server
We now need to modify `apps/kv/mix.exs` to contain the umbrella entries we have seen in `apps/kv_server/mix.exs`. Open up `apps/kv/mix.exs` and add to the `project/0` function:
```elixir
build_path: "../../_build",
config_path: "../../config/config.exs",
deps_path: "../../deps",
lockfile: "../../mix.lock",
```
Now you can run tests for both projects from the umbrella root with `mix test`. Sweet!
## Don't drink the kool aid
Umbrella projects are a convenience to help you organize and manage multiple applications. While it provides a degree of separation between applications, those applications are not fully decoupled, as they share the same configuration and the same dependencies.
The pattern of keeping multiple applications in the same repository is known as "mono-repo". Umbrella projects maximize this pattern by providing conveniences to compile, test and run multiple applications at once.
If you find yourself in a position where you want to use different configurations in each application for the same dependency or use different dependency versions, then it is likely your codebase has grown beyond what umbrellas can provide.
The good news is that breaking an umbrella apart is quite straightforward, as you simply need to move applications outside of the umbrella project's `apps/` directory and update the project's mix.exs file to no longer set the `build_path`, `config_path`, `deps_path`, and `lockfile` configuration. You can depend on private projects outside of the umbrella in multiple ways:
1. Move it to a separate folder within the same repository and point to it using a path dependency (the mono-repo pattern)
2. Move the repository to a separate Git repository and depend on it
3. Publish the project to a private [Hex.pm](https://hex.pm/) organization
## Summing up
In this chapter, we have learned more about Mix dependencies and umbrella projects. While we may run `kv` without a server, our `kv_server` depends directly on `kv`. By breaking them into separate applications, we gain more control in how they are developed and tested.
When using umbrella applications, it is important to have a clear boundary between them. Our upcoming `kv_server` must only access public APIs defined in `kv`. Think of your umbrella apps as any other dependency or even Elixir itself: you can only access what is public and documented. Reaching into private functionality in your dependencies is a poor practice that will eventually cause your code to break when a new version is up.
Umbrella applications can also be used as a stepping stone for eventually extracting an application from your codebase. For example, imagine a web application that has to send "push notifications" to its users. The whole "push notifications system" can be developed as a separate application in the umbrella, with its own supervision tree and APIs. If you ever run into a situation where another project needs the push notifications system, the system can be moved to a private repository or [a Hex package](https://hex.pm/).
Finally, keep in mind that applications in an umbrella project all share the same configurations and dependencies. If two applications in your umbrella need to configure the same dependency in drastically different ways or even use different versions, you have probably outgrown the benefits brought by umbrellas. Remember you can break the umbrella and still leverage the benefits behind "mono-repos".
With our umbrella project up and running, it is time to start writing our server.
@@ -1,359 +0,0 @@
<!--
SPDX-License-Identifier: Apache-2.0
SPDX-FileCopyrightText: 2021 The Elixir Team
-->
# Distributed tasks and tags
In this chapter, we will go back to the `:kv` application and add a routing layer that will allow us to distribute requests between nodes based on the bucket name.
The routing layer will receive a routing table of the following format:
```elixir
[
{?a..?m, :"foo@computer-name"},
{?n..?z, :"bar@computer-name"}
]
```
The router will check the first byte of the bucket name against the table and dispatch to the appropriate node based on that. For example, a bucket starting with the letter "a" (`?a` represents the Unicode codepoint of the letter "a") will be dispatched to node `foo@computer-name`.
If the matching entry points to the node evaluating the request, then we've finished routing, and this node will perform the requested operation. If the matching entry points to a different node, we'll pass the request to said node, which will look at its own routing table (which may be different from the one in the first node) and act accordingly. If no entry matches, an error will be raised.
> Note: we will be using two nodes in the same machine throughout this chapter. You are free to use two (or more) different machines on the same network but you need to do some prep work. First of all, you need to ensure all machines have a `~/.erlang.cookie` file with exactly the same value. Then you need to guarantee [epmd](https://www.erlang.org/doc/apps/erts/epmd_cmd) is running on a port that is not blocked (you can run `epmd -d` for debug info).
## Our first distributed code
Elixir ships with facilities to connect nodes and exchange information between them. In fact, we use the same concepts of processes, message passing and receiving messages when working in a distributed environment because Elixir processes are *location transparent*. This means that when sending a message, it doesn't matter if the recipient process is on the same node or on another node, the VM will be able to deliver the message in both cases.
In order to run distributed code, we need to start the VM with a name. The name can be short (when in the same network) or long (requires the full computer address). Let's start a new IEx session:
```console
$ iex --sname foo
```
You can see now the prompt is slightly different and shows the node name followed by the computer name:
Interactive Elixir - press Ctrl+C to exit (type h() ENTER for help)
iex(foo@jv)1>
My computer is named `jv`, so I see `foo@jv` in the example above, but you will get a different result. We will use `foo@computer-name` in the following examples and you should update them accordingly when trying out the code.
Let's define a module named `Hello` in this shell:
```elixir
iex> defmodule Hello do
...> def world, do: IO.puts("hello world")
...> end
```
If you have another computer on the same network with both Erlang and Elixir installed, you can start another shell on it. If you don't, you can start another IEx session in another terminal. In either case, give it the short name of `bar`:
```console
$ iex --sname bar
```
Note that inside this new IEx session, we cannot access `Hello.world/0`:
```elixir
iex> Hello.world
** (UndefinedFunctionError) function Hello.world/0 is undefined (module Hello is not available)
Hello.world()
```
However, we can spawn a new process on `foo@computer-name` from `bar@computer-name`! Let's give it a try (where `@computer-name` is the one you see locally):
```elixir
iex> Node.spawn_link(:"foo@computer-name", fn -> Hello.world() end)
#PID<9014.59.0>
hello world
```
Elixir spawned a process on another node and returned its PID. The code then executed on the other node where the `Hello.world/0` function exists and invoked that function. Note that the result of "hello world" was printed on the current node `bar` and not on `foo`. In other words, the message to be printed was sent back from `foo` to `bar`. This happens because the process spawned on the other node (`foo`) knows all the output should be sent back to the original node!
We can send and receive messages from the PID returned by `Node.spawn_link/2` as usual. Let's try a quick ping-pong example:
```elixir
iex> pid = Node.spawn_link(:"foo@computer-name", fn ->
...> receive do
...> {:ping, client} -> send(client, :pong)
...> end
...> end)
#PID<9014.59.0>
iex> send(pid, {:ping, self()})
{:ping, #PID<0.73.0>}
iex> flush()
:pong
:ok
```
From our quick exploration, we could conclude that we should use `Node.spawn_link/2` to spawn processes on a remote node every time we need to do a distributed computation. However, we have learned throughout this guide that spawning processes outside of supervision trees should be avoided if possible, so we need to look for other options.
There are three better alternatives to `Node.spawn_link/2` that we could use in our implementation:
1. We could use Erlang's [`:erpc`](`:erpc`) module to execute functions on a remote node. Inside the `bar@computer-name` shell above, you can call `:erpc.call(:"foo@computer-name", Hello, :world, [])` and it will print "hello world"
2. We could have a server running on the other node and send requests to that node via the `GenServer` API. For example, you can call a server on a remote node by using `GenServer.call({name, node}, arg)` or passing the remote process PID as the first argument
3. We could use [tasks](`Task`), which we have learned about in [a previous chapter](task-and-gen-tcp.md), as they can be spawned on both local and remote nodes
The options above have different properties. The GenServer would serialize your requests on a single server, while tasks are effectively running asynchronously on the remote node, with the only serialization point being the spawning done by the supervisor.
For our routing layer, we are going to use tasks, but feel free to explore the other alternatives too.
## async/await
So far we have explored tasks that are started and run in isolation, without regard to their return value. However, sometimes it is useful to run a task to compute a value and read its result later on. For this, tasks also provide the `async/await` pattern:
```elixir
task = Task.async(fn -> compute_something_expensive() end)
res = compute_something_else()
res + Task.await(task)
```
`async/await` provides a very simple mechanism to compute values concurrently. Not only that, `async/await` can also be used with the same `Task.Supervisor` we have used in previous chapters. We just need to call `Task.Supervisor.async/2` instead of `Task.Supervisor.start_child/2` and use `Task.await/2` to read the result later on.
## Distributed tasks
Distributed tasks are exactly the same as supervised tasks. The only difference is that we pass the node name when spawning the task on the supervisor. Open up `lib/kv/supervisor.ex` from the `:kv` application. Let's add a task supervisor as the last child of the tree:
```elixir
{Task.Supervisor, name: KV.RouterTasks},
```
Now, let's start two named nodes again, but inside the `:kv` application:
```console
$ iex --sname foo -S mix
$ iex --sname bar -S mix
```
From inside `bar@computer-name`, we can now spawn a task directly on the other node via the supervisor:
```elixir
iex> task = Task.Supervisor.async({KV.RouterTasks, :"foo@computer-name"}, fn ->
...> {:ok, node()}
...> end)
%Task{
mfa: {:erlang, :apply, 2},
owner: #PID<0.122.0>,
pid: #PID<12467.88.0>,
ref: #Reference<0.0.0.400>
}
iex> Task.await(task)
{:ok, :"foo@computer-name"}
```
Our first distributed task retrieves the name of the node the task is running on. Notice we have given an anonymous function to `Task.Supervisor.async/2` but, in distributed cases, it is preferable to give the module, function, and arguments explicitly:
```elixir
iex> task = Task.Supervisor.async({KV.RouterTasks, :"foo@computer-name"}, Kernel, :node, [])
%Task{
mfa: {Kernel, :node, 0},
owner: #PID<0.122.0>,
pid: #PID<12467.89.0>,
ref: #Reference<0.0.0.404>
}
iex> Task.await(task)
:"foo@computer-name"
```
The difference is that anonymous functions require the target node to have exactly the same code version as the caller. Using module, function, and arguments is more robust because you only need to find a function with matching arity in the given module.
With this knowledge in hand, let's finally write the routing code.
## Routing layer
Create a file at `lib/kv/router.ex` with the following contents:
```elixir
defmodule KV.Router do
@doc """
Dispatch the given `mod`, `fun`, `args` request
to the appropriate node based on the `bucket`.
"""
def route(bucket, mod, fun, args) do
# Get the first byte of the binary
first = :binary.first(bucket)
# Try to find an entry in the table() or raise
entry =
Enum.find(table(), fn {enum, _node} ->
first in enum
end) || no_entry_error(bucket)
# If the entry node is the current node
if elem(entry, 1) == node() do
apply(mod, fun, args)
else
{KV.RouterTasks, elem(entry, 1)}
|> Task.Supervisor.async(KV.Router, :route, [bucket, mod, fun, args])
|> Task.await()
end
end
defp no_entry_error(bucket) do
raise "could not find entry for #{inspect bucket} in table #{inspect table()}"
end
@doc """
The routing table.
"""
def table do
# Replace computer-name with your local machine name
[{?a..?m, :"foo@computer-name"}, {?n..?z, :"bar@computer-name"}]
end
end
```
Let's write a test to verify our router works. Create a file named `test/kv/router_test.exs` containing:
```elixir
defmodule KV.RouterTest do
use ExUnit.Case, async: true
test "route requests across nodes" do
assert KV.Router.route("hello", Kernel, :node, []) ==
:"foo@computer-name"
assert KV.Router.route("world", Kernel, :node, []) ==
:"bar@computer-name"
end
test "raises on unknown entries" do
assert_raise RuntimeError, ~r/could not find entry/, fn ->
KV.Router.route(<<0>>, Kernel, :node, [])
end
end
end
```
The first test invokes `Kernel.node/0`, which returns the name of the current node, based on the bucket names "hello" and "world". According to our routing table so far, we should get `foo@computer-name` and `bar@computer-name` as responses, respectively.
The second test checks that the code raises for unknown entries.
In order to run the first test, we need to have two nodes running. Move into `apps/kv` and let's restart the node named `bar` which is going to be used by tests.
```console
$ iex --sname bar -S mix
```
And now run tests with:
```console
$ elixir --sname foo -S mix test
```
The test should pass.
## Test filters and tags
Although our tests pass, our testing structure is getting more complex. In particular, running tests with only `mix test` causes failures in our suite, since our test requires a connection to another node.
Luckily, ExUnit ships with a facility to tag tests, allowing us to run specific callbacks or even filter tests altogether based on those tags. We have already used the `:capture_log` tag in the previous chapter, which has its semantics specified by ExUnit itself.
This time let's add a `:distributed` tag to `test/kv/router_test.exs`:
```elixir
@tag :distributed
test "route requests across nodes" do
```
Writing `@tag :distributed` is equivalent to writing `@tag distributed: true`.
With the test properly tagged, we can now check if the node is alive on the network and, if not, we can exclude all distributed tests. Open up `test/test_helper.exs` inside the `:kv` application and add the following:
```elixir
exclude =
if Node.alive?(), do: [], else: [distributed: true]
ExUnit.start(exclude: exclude)
```
Now run tests with `mix test`:
```console
$ mix test
Excluding tags: [distributed: true]
.......
Finished in 0.05 seconds
9 tests, 0 failures, 1 excluded
```
This time all tests passed and ExUnit warned us that distributed tests were being excluded. If you run tests with `$ elixir --sname foo -S mix test`, one extra test should run and successfully pass as long as the `bar@computer-name` node is available.
The `mix test` command also allows us to dynamically include and exclude tags. For example, we can run `$ mix test --include distributed` to run distributed tests regardless of the value set in `test/test_helper.exs`. We could also pass `--exclude` to exclude a particular tag from the command line. Finally, `--only` can be used to run only tests with a particular tag:
```console
$ elixir --sname foo -S mix test --only distributed
```
You can read more about filters, tags, and the default tags in the `ExUnit.Case` module documentation.
## Wiring it all up
Now with our routing system in place, let's change `KVServer` to use the router. Replace the `lookup/2` function in `KVServer.Command` from this:
```elixir
defp lookup(bucket, callback) do
case KV.Registry.lookup(KV.Registry, bucket) do
{:ok, pid} -> callback.(pid)
:error -> {:error, :not_found}
end
end
```
by this:
```elixir
defp lookup(bucket, callback) do
case KV.Router.route(bucket, KV.Registry, :lookup, [KV.Registry, bucket]) do
{:ok, pid} -> callback.(pid)
:error -> {:error, :not_found}
end
end
```
Instead of directly looking up the registry, we are using the router instead to match a specific node. Then we get a `pid` that can be from any process in our cluster. From now on, `GET`, `PUT` and `DELETE` requests are all routed to the appropriate node.
Let's also make sure that when a new bucket is created it ends up on the correct node. Replace the `run/1` function in `KVServer.Command`, the one that matches the `:create` command, with the following:
```elixir
def run({:create, bucket}) do
case KV.Router.route(bucket, KV.Registry, :create, [KV.Registry, bucket]) do
pid when is_pid(pid) -> {:ok, "OK\r\n"}
_ -> {:error, "FAILED TO CREATE BUCKET"}
end
end
```
Now if you run the tests, you will see that an existing test that checks the server interaction will fail, as it will attempt to use the routing table. To address this failure, change the `test_helper.exs` for `:kv_server` application as we did for `:kv` and add `@tag :distributed` to this test too:
```elixir
@tag :distributed
test "server interaction", %{socket: socket} do
```
However, keep in mind that by making the test distributed, we will likely run it less frequently, since we may not do the distributed setup on every test run. We will learn how to address this in the next chapter, by effectively learning how to make the routing table configurable.
## Summing up
We have only scratched the surface of what is possible when it comes to distribution.
In all of our examples, we relied on Erlang's ability to automatically connect nodes whenever there is a request. For example, when we invoked `Node.spawn_link(:"foo@computer-name", fn -> Hello.world() end)`, Erlang automatically connected to said node and started a new process. However, you may also want to take a more explicit approach to connections, by using `Node.connect/1` and `Node.disconnect/1`.
By default, Erlang establishes a fully meshed network, which means all nodes are connected to each other. Under this topology, the Erlang distribution is known to scale to several dozens of nodes in the same cluster. Erlang also has the concept of hidden nodes, which can allow developers to assemble custom topologies as seen in projects such as [Partisan](https://github.com/lasp-lang/partisan).
In production, you may have nodes connecting and disconnecting at any time. In such scenarios, you need to provide *node discoverability*. Libraries such as [libcluster](https://github.com/bitwalker/libcluster/) and [dns_cluster](https://github.com/phoenixframework/dns_cluster) provide several strategies for node discoverability using DNS, Kubernetes, etc.
Distributed key-value stores, used in real-life, need to consider the fact nodes may go up and down at any time and also migrate the bucket across nodes. Even further, buckets often need to be duplicated between nodes, so a failure in a node does not lead to the whole bucket being lost. This process is called *replication*. Our implementation won't attempt to tackle such problems. Instead, we assume there is a fixed number of nodes and therefore use a fixed routing table.
These topics can be daunting at first but remember that most Elixir frameworks abstract those concerns for you. For example, when using [the Phoenix web framework](https://phoenixframework.org), its plug-and-play abstractions take care of sending messages and tracking how users join and leave a cluster. However, if you are interested in distributed systems after all, there is much to explore. Here are some additional references:
* [The excellent Distribunomicon chapter from Learn You Some Erlang](http://learnyousomeerlang.com/distribunomicon)
* Erlang's [`:global` module](`:global`), which can provide global names and global locks, allowing unique names and unique locks in a whole cluster of machines
* Erlang's [`:pg` module](`:pg`), which allows process to join different groups shared across the whole cluster
* [Phoenix PubSub project](https://github.com/phoenixframework/phoenix_pubsub), which provides a distributed messaging system and a distributed presence system for tracking users and processes in a cluster
You will also find many libraries for building distributed systems within the overall Erlang ecosystem. For now, it is time to go back to our simple distributed key-value store and learn how to configure and package it for production.
@@ -25,7 +25,7 @@ DELETE shopping eggs
OK
```
After the parsing is done, we will update our server to dispatch the parsed commands to the `:kv` application we built previously.
After the parsing is done, we will update our server to dispatch the parsed commands to the relevant buckets.
## Doctests
@@ -33,16 +33,16 @@ On the language homepage, we mention that Elixir makes documentation a first-cla
In this section, we will implement the parsing functionality, document it and make sure our documentation is up to date with doctests. This helps us provide documentation with accurate code samples.
Let's create our command parser at `lib/kv_server/command.ex` and start with the doctest:
Let's create our command parser at `lib/kv/command.ex` and start with the doctest:
```elixir
defmodule KVServer.Command do
defmodule KV.Command do
@doc ~S"""
Parses the given `line` into a command.
## Examples
iex> KVServer.Command.parse("CREATE shopping\r\n")
iex> KV.Command.parse("CREATE shopping\r\n")
{:ok, {:create, "shopping"}}
"""
@@ -56,29 +56,29 @@ Doctests are specified by an indentation of four spaces followed by the `iex>` p
Also, note that we started the documentation string using `@doc ~S"""`. The `~S` prevents the `\r\n` characters from being converted to a carriage return and line feed until they are evaluated in the test.
To run our doctests, we'll create a file at `test/kv_server/command_test.exs` and call `doctest KVServer.Command` in the test case:
To run our doctests, we'll create a file at `test/kv/command_test.exs` and call `doctest KV.Command` in the test case:
```elixir
defmodule KVServer.CommandTest do
defmodule KV.CommandTest do
use ExUnit.Case, async: true
doctest KVServer.Command
doctest KV.Command
end
```
Run the test suite and the doctest should fail:
```text
1) doctest KVServer.Command.parse/1 (1) (KVServer.CommandTest)
test/kv_server/command_test.exs:3
1) doctest KV.Command.parse/1 (1) (KV.CommandTest)
test/kv/command_test.exs:3
Doctest failed
doctest:
iex> KVServer.Command.parse("CREATE shopping\r\n")
iex> KV.Command.parse("CREATE shopping\r\n")
{:ok, {:create, "shopping"}}
code: KVServer.Command.parse "CREATE shopping\r\n" === {:ok, {:create, "shopping"}}
code: KV.Command.parse "CREATE shopping\r\n" === {:ok, {:create, "shopping"}}
left: :not_implemented
right: {:ok, {:create, "shopping"}}
stacktrace:
lib/kv_server/command.ex:7: KVServer.Command (module)
lib/kv/command.ex:7: KV.Command (module)
```
Excellent!
@@ -96,50 +96,50 @@ end
Our implementation splits the line on whitespace and then matches the command against a list. Using `String.split/1` means our commands will be whitespace-insensitive. Leading and trailing whitespace won't matter, nor will consecutive spaces between words. Let's add some new doctests to test this behavior along with the other commands:
```elixir
@doc ~S"""
Parses the given `line` into a command.
@doc ~S"""
Parses the given `line` into a command.
## Examples
## Examples
iex> KVServer.Command.parse "CREATE shopping\r\n"
{:ok, {:create, "shopping"}}
iex> KV.Command.parse "CREATE shopping\r\n"
{:ok, {:create, "shopping"}}
iex> KVServer.Command.parse "CREATE shopping \r\n"
{:ok, {:create, "shopping"}}
iex> KV.Command.parse "CREATE shopping \r\n"
{:ok, {:create, "shopping"}}
iex> KVServer.Command.parse "PUT shopping milk 1\r\n"
{:ok, {:put, "shopping", "milk", "1"}}
iex> KV.Command.parse "PUT shopping milk 1\r\n"
{:ok, {:put, "shopping", "milk", "1"}}
iex> KVServer.Command.parse "GET shopping milk\r\n"
{:ok, {:get, "shopping", "milk"}}
iex> KV.Command.parse "GET shopping milk\r\n"
{:ok, {:get, "shopping", "milk"}}
iex> KVServer.Command.parse "DELETE shopping eggs\r\n"
{:ok, {:delete, "shopping", "eggs"}}
iex> KV.Command.parse "DELETE shopping eggs\r\n"
{:ok, {:delete, "shopping", "eggs"}}
Unknown commands or commands with the wrong number of
arguments return an error:
Unknown commands or commands with the wrong number of
arguments return an error:
iex> KVServer.Command.parse "UNKNOWN shopping eggs\r\n"
{:error, :unknown_command}
iex> KV.Command.parse "UNKNOWN shopping eggs\r\n"
{:error, :unknown_command}
iex> KVServer.Command.parse "GET shopping\r\n"
{:error, :unknown_command}
iex> KV.Command.parse "GET shopping\r\n"
{:error, :unknown_command}
"""
"""
```
With doctests at hand, it is your turn to make tests pass! Once you're ready, you can compare your work with our solution below:
```elixir
def parse(line) do
case String.split(line) do
["CREATE", bucket] -> {:ok, {:create, bucket}}
["GET", bucket, key] -> {:ok, {:get, bucket, key}}
["PUT", bucket, key, value] -> {:ok, {:put, bucket, key, value}}
["DELETE", bucket, key] -> {:ok, {:delete, bucket, key}}
_ -> {:error, :unknown_command}
def parse(line) do
case String.split(line) do
["CREATE", bucket] -> {:ok, {:create, bucket}}
["GET", bucket, key] -> {:ok, {:get, bucket, key}}
["PUT", bucket, key, value] -> {:ok, {:put, bucket, key, value}}
["DELETE", bucket, key] -> {:ok, {:delete, bucket, key}}
_ -> {:error, :unknown_command}
end
end
end
```
Notice how we were able to elegantly parse the commands without adding a bunch of `if/else` clauses that check the command name and number of arguments!
@@ -147,104 +147,107 @@ Notice how we were able to elegantly parse the commands without adding a bunch o
Finally, you may have observed that each doctest corresponds to a different test in our suite, which now reports a total of 7 doctests. That is because ExUnit considers the following to define two different doctests:
```elixir
iex> KVServer.Command.parse("UNKNOWN shopping eggs\r\n")
iex> KV.Command.parse("UNKNOWN shopping eggs\r\n")
{:error, :unknown_command}
iex> KVServer.Command.parse("GET shopping\r\n")
iex> KV.Command.parse("GET shopping\r\n")
{:error, :unknown_command}
```
Without new lines, as seen below, ExUnit compiles it into a single doctest:
```elixir
iex> KVServer.Command.parse("UNKNOWN shopping eggs\r\n")
iex> KV.Command.parse("UNKNOWN shopping eggs\r\n")
{:error, :unknown_command}
iex> KVServer.Command.parse("GET shopping\r\n")
iex> KV.Command.parse("GET shopping\r\n")
{:error, :unknown_command}
```
As the name says, doctest is documentation first and a test later. Their goal is not to replace tests but to provide up-to-date documentation. You can read more about doctests in the `ExUnit.DocTest` documentation.
## `with`
## Using `with`
As we are now able to parse commands, we can finally start implementing the logic that runs the commands. Let's add a stub definition for this function for now:
```elixir
defmodule KVServer.Command do
defmodule KV.Command do
@doc """
Runs the given command.
"""
def run(command) do
{:ok, "OK\r\n"}
def run(command, socket) do
:gen_tcp.send(socket, "OK\r\n")
:ok
end
end
```
Before we implement this function, let's change our server to start using our new `parse/1` and `run/1` functions. Remember, our `read_line/1` function was also crashing when the client closed the socket, so let's take the opportunity to fix it, too. Open up `lib/kv_server.ex` and replace the existing server definition:
Before we implement this function, let's change our server to start using our new `parse/1` and `run/1` functions. Remember, our `read_line/1` function was also crashing when the client closed the socket, so let's take the opportunity to fix it, too. Open up `lib/kv/server.ex` and replace the existing server definition:
```elixir
defp serve(socket) do
socket
|> read_line()
|> write_line(socket)
defp serve(socket) do
socket
|> read_line()
|> write_line(socket)
serve(socket)
end
serve(socket)
end
defp read_line(socket) do
{:ok, data} = :gen_tcp.recv(socket, 0)
data
end
defp read_line(socket) do
{:ok, data} = :gen_tcp.recv(socket, 0)
data
end
defp write_line(line, socket) do
:gen_tcp.send(socket, line)
end
defp write_line(line, socket) do
:gen_tcp.send(socket, line)
end
```
by the following:
```elixir
defp serve(socket) do
msg =
case read_line(socket) do
{:ok, data} ->
case KVServer.Command.parse(data) do
{:ok, command} ->
KVServer.Command.run(command)
{:error, _} = err ->
err
end
{:error, _} = err ->
err
end
defp serve(socket) do
msg =
case read_line(socket) do
{:ok, data} ->
case KV.Command.parse(data) do
{:ok, command} ->
KV.Command.run(command, socket)
write_line(socket, msg)
serve(socket)
end
{:error, _} = err ->
err
end
defp read_line(socket) do
:gen_tcp.recv(socket, 0)
end
{:error, _} = err ->
err
end
defp write_line(socket, {:ok, text}) do
:gen_tcp.send(socket, text)
end
write_line(socket, msg)
serve(socket)
end
defp write_line(socket, {:error, :unknown_command}) do
# Known error; write to the client
:gen_tcp.send(socket, "UNKNOWN COMMAND\r\n")
end
defp read_line(socket) do
:gen_tcp.recv(socket, 0)
end
defp write_line(_socket, {:error, :closed}) do
# The connection was closed, exit politely
exit(:shutdown)
end
defp write_line(_socket, :ok) do
:ok
end
defp write_line(socket, {:error, error}) do
# Unknown error; write to the client and exit
:gen_tcp.send(socket, "ERROR\r\n")
exit(error)
end
defp write_line(socket, {:error, :unknown_command}) do
# Known error; write to the client
:gen_tcp.send(socket, "UNKNOWN COMMAND\r\n")
end
defp write_line(_socket, {:error, :closed}) do
# The connection was closed, exit politely
exit(:shutdown)
end
defp write_line(socket, {:error, error}) do
# Unknown error; write to the client and exit
:gen_tcp.send(socket, "ERROR\r\n")
exit(error)
end
```
If we start our server, we can now send commands to it. For now, we will get two different responses: "OK" when the command is known and "UNKNOWN COMMAND" otherwise:
@@ -264,18 +267,18 @@ This means our implementation is going in the correct direction, but it doesn't
The previous implementation used pipelines which made the logic straightforward to follow. However, now that we need to handle different error codes along the way, our server logic is nested inside many `case` calls.
Thankfully, Elixir v1.2 introduced the `with` construct, which allows you to simplify code like the above, replacing nested `case` calls with a chain of matching clauses. Let's rewrite the `serve/1` function to use `with`:
Thankfully, Elixir has the `with` construct, which allows you to simplify code like the above, replacing nested `case` calls with a chain of matching clauses. Let's rewrite the `serve/1` function to use `with`:
```elixir
defp serve(socket) do
msg =
with {:ok, data} <- read_line(socket),
{:ok, command} <- KVServer.Command.parse(data),
do: KVServer.Command.run(command)
defp serve(socket) do
msg =
with {:ok, data} <- read_line(socket),
{:ok, command} <- KV.Command.parse(data),
do: KV.Command.run(command, socket)
write_line(socket, msg)
serve(socket)
end
write_line(socket, msg)
serve(socket)
end
```
Much better! `with` will retrieve the value returned by the right-side of `<-` and match it against the pattern on the left side. If the value matches the pattern, `with` moves on to the next expression. In case there is no match, the non-matching value is returned.
@@ -286,55 +289,60 @@ You can read more about `with/1` in our documentation.
## Running commands
The last step is to implement `KVServer.Command.run/1`, to run the parsed commands against the `:kv` application. Its implementation is shown below:
The last step is to implement `KV.Command.run/1` to run the parsed commands on top of buckets. Its implementation is shown below:
```elixir
@doc """
Runs the given command.
"""
def run(command)
@doc """
Runs the given command.
"""
def run(command, socket)
def run({:create, bucket}) do
KV.Registry.create(KV.Registry, bucket)
{:ok, "OK\r\n"}
end
def run({:create, bucket}, socket) do
KV.create_bucket(bucket)
:gen_tcp.send(socket, "OK\r\n")
:ok
end
def run({:get, bucket, key}) do
lookup(bucket, fn pid ->
value = KV.Bucket.get(pid, key)
{:ok, "#{value}\r\nOK\r\n"}
end)
end
def run({:get, bucket, key}, socket) do
lookup(bucket, fn pid ->
value = KV.Bucket.get(pid, key)
:gen_tcp.send(socket, "#{value}\r\nOK\r\n")
:ok
end)
end
def run({:put, bucket, key, value}) do
lookup(bucket, fn pid ->
KV.Bucket.put(pid, key, value)
{:ok, "OK\r\n"}
end)
end
def run({:put, bucket, key, value}, socket) do
lookup(bucket, fn pid ->
KV.Bucket.put(pid, key, value)
:gen_tcp.send(socket, "OK\r\n")
:ok
end)
end
def run({:delete, bucket, key}) do
lookup(bucket, fn pid ->
KV.Bucket.delete(pid, key)
{:ok, "OK\r\n"}
end)
end
def run({:delete, bucket, key}, socket) do
lookup(bucket, fn pid ->
KV.Bucket.delete(pid, key)
:gen_tcp.send(socket, "OK\r\n")
:ok
end)
end
defp lookup(bucket, callback) do
case KV.Registry.lookup(KV.Registry, bucket) do
{:ok, pid} -> callback.(pid)
:error -> {:error, :not_found}
defp lookup(bucket, callback) do
if bucket = KV.lookup_bucket(bucket) do
callback.(bucket)
else
{:error, :not_found}
end
end
end
```
Every function clause dispatches the appropriate command to the `KV.Registry` server that we registered during the `:kv` application startup. Since our `:kv_server` depends on the `:kv` application, it is completely fine to depend on the services it provides.
Each function clause dispatches the appropriate command to the appropriate bucket.
You might have noticed we have a function head, `def run(command)`, without a body. In the [Modules and Functions](../getting-started/modules-and-functions.md#default-arguments) chapter, we learned that a bodiless function can be used to declare default arguments for a multi-clause function. Here is another use case where we use a function without a body to document what the arguments are.
You might have noticed we have a function head, `def run(command, socket)`, without a body. In the [Modules and Functions](../getting-started/modules-and-functions.md#default-arguments) chapter, we learned that a bodiless function can be used to declare default arguments for a multi-clause function. Here is another use case where we use a function without a body to document what the arguments are.
Note that we have also defined a private function named `lookup/2` to help with the common functionality of looking up a bucket and returning its `pid` if it exists, `{:error, :not_found}` otherwise.
We have also defined a private function named `lookup/2` to help with the common functionality of looking up a bucket and returning its `pid` if it exists, `{:error, :not_found}` otherwise.
By the way, since we are now returning `{:error, :not_found}`, we should amend the `write_line/2` function in `KVServer` to print such error as well:
By the way, since we are now returning `{:error, :not_found}`, we should amend the `write_line/2` function in `KV.Server` to print such error as well:
```elixir
defp write_line(socket, {:error, :not_found}) do
@@ -342,67 +350,57 @@ defp write_line(socket, {:error, :not_found}) do
end
```
Our server functionality is almost complete. Only tests are missing. This time, we have left tests for last because there are some important considerations to be made.
Our server functionality is almost complete. Only tests are missing.
`KVServer.Command.run/1`'s implementation is sending commands directly to the server named `KV.Registry`, which is registered by the `:kv` application. This means this server is global and if we have two tests sending messages to it at the same time, our tests will conflict with each other (and likely fail). We need to decide between having unit tests that are isolated and can run asynchronously, or writing integration tests that work on top of the global state, but exercise our application's full stack as it is meant to be exercised in production.
## Integration tests
So far we have only written unit tests, typically testing a single module directly. However, in order to make `KVServer.Command.run/1` testable as a unit we would need to change its implementation to not send commands directly to the `KV.Registry` process but instead pass a server as an argument. For example, we would need to change `run`'s signature to `def run(command, pid)` and then change all clauses accordingly:
`KV.Command.run/1`'s implementation is sending commands directly to the `KV` module, which is using a local registry to name processes. This means if we have two tests sending messages to the same bucket, our tests will conflict with each other (and likely fail). One might think this would be a reason to use mocks and other strategies to keep our tests isolated, but such techniques often make our testing environment too distant from how our code actually runs in production, and you may end-up with bugs lurking.
Luckily, there is a technique that we have been using throughout this guide that would be equally applicable here: it is ok to rely on the local registry as long as each test uses unique names. Using a combination of the test module and test name is more than enough to guarantee that.
So let's write integration tests that rely on unique names to exercise the whole stack from the TCP server to the bucket.
Create a new file at `test/kv/server_test.exs` as shown below:
```elixir
def run({:create, bucket}, pid) do
KV.Registry.create(pid, bucket)
{:ok, "OK\r\n"}
end
defmodule KV.ServerTest do
use ExUnit.Case, async: true
# ... other run clauses ...
```
@socket_options [:binary, packet: :line, active: false]
Feel free to go ahead and do the changes above and write some unit tests. The idea is that your tests will start an instance of the `KV.Registry` and pass it as an argument to `run/2` instead of relying on the global `KV.Registry`. This has the advantage of keeping our tests asynchronous as there is no shared state.
But let's also try something different. Let's write integration tests that rely on the global server names to exercise the whole stack from the TCP server to the bucket. Our integration tests will rely on global state and must be synchronous. With integration tests, we get coverage on how the components in our application work together at the cost of test performance. They are typically used to test the main flows in your application. For example, we should avoid using integration tests to test an edge case in our command parsing implementation.
Our integration test will use a TCP client that sends commands to our server and assert we are getting the desired responses.
Let's implement the integration test in `test/kv_server_test.exs` as shown below:
```elixir
defmodule KVServerTest do
use ExUnit.Case
setup do
Application.stop(:kv)
:ok = Application.start(:kv)
setup config do
{:ok, socket} = :gen_tcp.connect(~c"localhost", 4040, @socket_options)
test_name = config.test |> Atom.to_string() |> String.replace(" ", "-")
%{socket: socket, name: "#{config.module}-#{test_name}"}
end
setup do
opts = [:binary, packet: :line, active: false]
{:ok, socket} = :gen_tcp.connect(~c"localhost", 4040, opts)
%{socket: socket}
test "server interaction", %{socket: socket, name: name} do
# CREATE
assert send_and_recv(socket, "CREATE #{name}\r\n") == "OK\r\n"
# PUT
assert send_and_recv(socket, "PUT #{name} eggs 3\r\n") == "OK\r\n"
# GET
assert send_and_recv(socket, "GET #{name} eggs\r\n") == "3\r\n"
assert send_and_recv(socket, "") == "OK\r\n"
# DELETE
assert send_and_recv(socket, "DELETE #{name} eggs\r\n") == "OK\r\n"
# GET
assert send_and_recv(socket, "GET #{name} eggs\r\n") == "\r\n"
assert send_and_recv(socket, "") == "OK\r\n"
end
test "server interaction", %{socket: socket} do
assert send_and_recv(socket, "UNKNOWN shopping\r\n") ==
"UNKNOWN COMMAND\r\n"
test "unknown command", %{socket: socket} do
assert send_and_recv(socket, "WHATEVER\r\n") ==
"UNKNOWN COMMAND\r\n"
end
assert send_and_recv(socket, "GET shopping eggs\r\n") ==
"NOT FOUND\r\n"
assert send_and_recv(socket, "CREATE shopping\r\n") ==
"OK\r\n"
assert send_and_recv(socket, "PUT shopping eggs 3\r\n") ==
"OK\r\n"
# GET returns two lines
assert send_and_recv(socket, "GET shopping eggs\r\n") == "3\r\n"
assert send_and_recv(socket, "") == "OK\r\n"
assert send_and_recv(socket, "DELETE shopping eggs\r\n") ==
"OK\r\n"
# GET returns two lines
assert send_and_recv(socket, "GET shopping eggs\r\n") == "\r\n"
assert send_and_recv(socket, "") == "OK\r\n"
test "unknown bucket", %{socket: socket} do
assert send_and_recv(socket, "GET whatever eggs\r\n") ==
"NOT FOUND\r\n"
end
defp send_and_recv(socket, command) do
@@ -413,38 +411,16 @@ defmodule KVServerTest do
end
```
Our integration test checks all server interaction, including unknown commands and not found errors. It is worth noting that, as with ETS tables and linked processes, there is no need to close the socket. Once the test process exits, the socket is automatically closed.
Run `mix test` and the tests should all pass. However, make sure to terminate any `iex -S mix` session you may have running, as currently tests and development environment are running on the same port (4040). We will address it in the next chapter.
This time, since our test relies on global data, we have not given `async: true` to `use ExUnit.Case`. Furthermore, in order to guarantee our test is always in a clean state, we stop and start the `:kv` application before each test. In fact, stopping the `:kv` application even prints a warning on the terminal:
We added three tests, the first one tests most bucket actions, while the other two deal with error cases. Given there is a lot of shared setup across these tests, we used the `setup/2` macro to deal with common boilerplate. The macro receives the same *test context* as tests and starts a client TCP connection per test. It also defines a unique bucket name using the module name and the test name, making sure any space in the test name is replaced by `-` as to not interfere with our command parsing logic.
```text
18:12:10.698 [info] Application kv exited: :stopped
```
To avoid printing log messages during tests, ExUnit provides a neat feature called `:capture_log`. By setting `@tag :capture_log` before each test or `@moduletag :capture_log` for the whole test module, ExUnit will automatically capture anything that is logged while the test runs. In case our test fails, the captured logs will be printed alongside the ExUnit report.
Between `use ExUnit.Case` and `setup`, add the following call:
Then, in each test, we pattern matched on the *test context*, extracting the socket or name as necessary. This is similar to the code we wrote in `test/kv/bucket_test.exs`:
```elixir
@moduletag :capture_log
test "stores values by key on a named process", config do
```
In case the test crashes, you will see a report as follows:
Except back then we matched on all config and, this time around, we matched only on the data we needed.
```text
1) test server interaction (KVServerTest)
test/kv_server_test.exs:17
** (RuntimeError) oops
stacktrace:
test/kv_server_test.exs:29
The following output was logged:
13:44:10.035 [notice] Application kv exited: :stopped
```
With this simple integration test, we start to see why integration tests may be slow. Not only can this test not run asynchronously, but it also requires the expensive setup of stopping and starting the `:kv` application.
At the end of the day, it is up to you and your team to figure out the best testing strategy for your applications. You need to balance code quality, confidence, and test suite runtime. For example, we may start with testing the server only with integration tests, but if the server continues to grow in future releases, or it becomes a part of the application with frequent bugs, it is important to consider breaking it apart and writing more intensive unit tests that don't have the weight of an integration test.
Let's move to the next chapter. We will finally make our system distributed by adding a bucket routing mechanism. We will use this opportunity to also improve our testing chops.
Let's move to the next chapter. We will finally make our system distributed by adding a tiny bit of configuration and, *spoiler alert*, changing one line of code.
+172 -144
View File
@@ -5,161 +5,192 @@
# Supervising dynamic children
We have now successfully defined our supervisor which is automatically started (and stopped) as part of our application life cycle.
We have successfully learned how our supervision tree is automatically started (and stopped) as part of our application's life cycle. We can also name our buckets via the `:name` option. We also learned that, in practice, we should always start new processes inside supervisors. Let's apply these insights by ensuring our buckets are named and supervised.
Remember, however, that our `KV.Registry` is both linking (via `start_link`) and monitoring (via `monitor`) bucket processes in the `handle_cast/2` callback:
## Child specs
Supervisors know how to start processes because they are given "child specifications". In our `lib/kv.ex` file, we defined a list of children with a single child spec:
```elixir
{:ok, bucket} = KV.Bucket.start_link([])
ref = Process.monitor(bucket)
```
Links are bidirectional, which implies that a crash in a bucket will crash the registry. Although we now have the supervisor, which guarantees the registry will be back up and running, crashing the registry still means we lose all data associating bucket names to their respective processes.
In other words, we want the registry to keep on running even if a bucket crashes. Let's write a new registry test:
```elixir
test "removes bucket on crash", %{registry: registry} do
KV.Registry.create(registry, "shopping")
{:ok, bucket} = KV.Registry.lookup(registry, "shopping")
# Stop the bucket with non-normal reason
Agent.stop(bucket, :shutdown)
assert KV.Registry.lookup(registry, "shopping") == :error
end
```
The test is similar to "removes bucket on exit" except that we are being a bit more harsh by sending `:shutdown` as the exit reason instead of `:normal`. If a process terminates with a reason other than `:normal`, all linked processes receive an EXIT signal, causing the linked process to also terminate unless it is trapping exits.
Since the bucket terminated, the registry also stopped, and our test fails when trying to `GenServer.call/3` it:
```text
1) test removes bucket on crash (KV.RegistryTest)
test/kv/registry_test.exs:26
** (exit) exited in: GenServer.call(#PID<0.148.0>, {:lookup, "shopping"}, 5000)
** (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started
code: assert KV.Registry.lookup(registry, "shopping") == :error
stacktrace:
(elixir) lib/gen_server.ex:770: GenServer.call/3
test/kv/registry_test.exs:33: (test)
```
We are going to solve this issue by defining a new supervisor that will spawn and supervise all buckets. Opposite to the previous Supervisor we defined, the children are not known upfront, but they are rather started dynamically. For those situations, we use a supervisor optimized to such use cases called `DynamicSupervisor`. The `DynamicSupervisor` does not expect a list of children during initialization; instead each child is started manually via `DynamicSupervisor.start_child/2`.
## The bucket supervisor
Since a `DynamicSupervisor` does not define any children during initialization, the `DynamicSupervisor` also allows us to skip the work of defining a whole separate module with the usual `start_link` function and the `init` callback. Instead, we can define a `DynamicSupervisor` directly in the supervision tree, by giving it a name and a strategy.
Open up `lib/kv/supervisor.ex` and add the dynamic supervisor as a child as follows:
```elixir
def init(:ok) do
children = [
{KV.Registry, name: KV.Registry},
{Registry, name: KV, keys: :unique}
]
```
When the child specification is a tuple (as above) or module, then it is equivalent to calling the `child_spec/1` function on said module, which then returns the full specification. The pair above is equivalent to:
```elixir
iex> Registry.child_spec(name: KV, keys: :unique)
%{
id: KV,
start: {Registry, :start_link, [[name: KV, keys: :unique]]},
type: :supervisor
}
```
The underlying map returns the `:id` (required), the module-function-args triplet to invoke to start the process (required), the type of the process (optional), among other optional keys. In other words, the `child_spec/1` function allows us to compose and encapsulate specifications in modules.
Therefore, if we want to supervise `KV.Bucket`, we only need to define a `child_spec/1` function. Luckily for us, whenever we invoke `use Agent` (or `use GenServer` or `use Supervisor` and so forth), an implementation with reasonable defaults is provided. So let's take it for a spin. Back on `iex -S mix`, try this:
```elixir
iex> KV.Bucket.child_spec([])
%{id: KV.Bucket, start: {KV.Bucket, :start_link, [[]]}}
iex> KV.Bucket.child_spec([name: :shopping])
%{id: KV.Bucket, start: {KV.Bucket, :start_link, [[name: :shopping]]}}
```
Let's try to start it as part of a supervisor then, using the `{module, options}` format to pass the bucket name (let's also use an atom as the name for convenience):
```elixir
iex> children = [{KV.Bucket, name: :shopping}]
iex> Supervisor.start_link(children, strategy: :one_for_one)
iex> KV.Bucket.put(:shopping, "milk", 1)
:ok
iex> KV.Bucket.get(:shopping, "milk")
1
```
What happens now if we explicitly kill the bucket process?
```elixir
# Find the pid for the given name
iex> pid = Process.whereis(:shopping)
#PID<0.48.0>
# Send it a kill exit signal
iex> Process.exit(pid, :kill)
true
# But a new process is alive in its place
iex> Process.whereis(:shopping)
#PID<0.50.0>
```
Given our buckets can already be supervised, it is time to hook them into our supervision tree.
## Dynamic supervisors
Given our buckets can already be supervised, you may be thinking to start them as part of our application `start/2` callback, such as:
```elixir
children = [
{Registry, name: KV, keys: :unique}
{KV.Bucket, name: {:via, Registry, {KV, "shopping"}}}
]
```
And while the above would definitely work, it comes with a huge caveat: it only starts a single bucket. In practice, we want the user to be able to create new buckets at any time. In other words, we need to start and supervise processes dynamically.
While the `Supervisor` module has APIs for starting children after its initialization, it was not designed or optimized for the use case of having potentially millions of children. For this purpose, Elixir instead provides the `DynamicSupervisor` module. Using it is quite similar to `Supervisor` except that, instead of specifying the children during start, you do it afterwards. Let's take it for a spin:
```elixir
iex> {:ok, sup_pid} = DynamicSupervisor.start_link(strategy: :one_for_one)
iex> DynamicSupervisor.start_child(sup_pid, {KV.Bucket, name: :another_list})
iex> KV.Bucket.put(:another_list, "milk", 1)
:ok
iex> KV.Bucket.get(:another_list, "milk")
1
```
And it all works as expected. In fact, we can even give names to `DynamicSupervisor` themselves, instead of passing PIDs around and also use it to start buckets named using the registry:
```elixir
iex> DynamicSupervisor.start_link(strategy: :one_for_one, name: :dyn_sup)
iex> name = {:via, Registry, {KV, "yet_another_list"}}
iex> DynamicSupervisor.start_child(:dyn_sup, {KV.Bucket, name: name})
iex> KV.Bucket.put(name, "milk", 1)
:ok
iex> KV.Bucket.get(name, "milk")
1
```
Overall, processes can be named and supervised, regardless if they are supervisors, agents, etc, since all of Elixir standard library was designed around those capabilities.
With all ingredients in place to supervise and name buckets, open up the `lib/kv.ex` module and let's add a new function called `KV.lookup_bucket/1`, which receives a name and either create or returns a bucket for the given name:
```elixir
defmodule KV do
use Application
@impl true
def start(_type, _args) do
children = [
{Registry, name: KV, keys: :unique},
{DynamicSupervisor, name: KV.BucketSupervisor, strategy: :one_for_one}
]
Supervisor.init(children, strategy: :one_for_one)
Supervisor.start_link(children, strategy: :one_for_one)
end
```
Remember that the name of a process can be any atom. So far, we have named processes with the same name as the modules that define their implementation. For example, the process defined by `KV.Registry` was given a process name of `KV.Registry`. This is simply a convention: If later there is an error in your system that says, "process named KV.Registry crashed with reason", we know exactly where to investigate.
In this case, there is no module, so we picked the name `KV.BucketSupervisor`. It could have been any other name. We also chose the `:one_for_one` strategy, which is currently the only available strategy for dynamic supervisors.
Run `iex -S mix` so we can give our dynamic supervisor a try:
```elixir
iex> {:ok, bucket} = DynamicSupervisor.start_child(KV.BucketSupervisor, KV.Bucket)
{:ok, #PID<0.72.0>}
iex> KV.Bucket.put(bucket, "eggs", 3)
:ok
iex> KV.Bucket.get(bucket, "eggs")
3
```
`DynamicSupervisor.start_child/2` expects the name of the supervisor and the child specification of the child to be started.
The last step is to change the registry to use the dynamic supervisor:
```elixir
def handle_cast({:create, name}, {names, refs}) do
if Map.has_key?(names, name) do
{:noreply, {names, refs}}
else
{:ok, pid} = DynamicSupervisor.start_child(KV.BucketSupervisor, KV.Bucket)
ref = Process.monitor(pid)
refs = Map.put(refs, ref, name)
names = Map.put(names, name, pid)
{:noreply, {names, refs}}
end
@doc """
Creates a bucket with the given name.
"""
def create_bucket(name) do
DynamicSupervisor.start_child(KV.BucketSupervisor, {KV.Bucket, name: via(name)})
end
```
That's enough for our tests to pass but there is a resource leakage in our application. When a bucket terminates, the supervisor will start a new bucket in its place. After all, that's the role of the supervisor!
However, when the supervisor restarts the new bucket, the registry does not know about it. So we will have an empty bucket in the supervisor that nobody can access! To solve this, we want to say that buckets are actually temporary. If they crash, regardless of the reason, they should not be restarted.
We can do this by passing the `restart: :temporary` option to `use Agent` in `KV.Bucket`:
```elixir
defmodule KV.Bucket do
use Agent, restart: :temporary
```
Let's also add a test to `test/kv/bucket_test.exs` that guarantees the bucket is temporary:
```elixir
test "are temporary workers" do
assert Supervisor.child_spec(KV.Bucket, []).restart == :temporary
@doc """
Looks up the given bucket.
"""
def lookup_bucket(name) do
GenServer.whereis(via(name))
end
```
Our test uses the `Supervisor.child_spec/2` function to retrieve the child specification out of a module and then assert its restart value is `:temporary`. At this point, you may be wondering why use a supervisor if it never restarts its children. It happens that supervisors provide more than restarts, they are also responsible for guaranteeing proper startup and shutdown, especially in case of crashes in a supervision tree.
## Supervision trees
When we added `KV.BucketSupervisor` as a child of `KV.Supervisor`, we began to have supervisors that supervise other supervisors, forming so-called "supervision trees".
Every time you add a new child to a supervisor, it is important to evaluate if the supervisor strategy is correct as well as the order of child processes. In this case, we are using `:one_for_one` and the `KV.Registry` is started before `KV.BucketSupervisor`.
One flaw that shows up right away is the ordering issue. Since `KV.Registry` invokes `KV.BucketSupervisor`, then the `KV.BucketSupervisor` must be started before `KV.Registry`. Otherwise, it may happen that the registry attempts to reach the bucket supervisor before it has started.
The second flaw is related to the supervision strategy. If `KV.Registry` dies, all information linking `KV.Bucket` names to bucket processes is lost. Therefore the `KV.BucketSupervisor` and all children must terminate too - otherwise we will have orphan processes.
In light of this observation, we should consider moving to another supervision strategy. The two other candidates are `:one_for_all` and `:rest_for_one`. A supervisor using the `:rest_for_one` strategy will kill and restart child processes which were started *after* the crashed child. In this case, we would want `KV.BucketSupervisor` to terminate if `KV.Registry` terminates. This would require the bucket supervisor to be placed after the registry which violates the ordering constraints we have established two paragraphs above.
So our last option is to go all in and pick the `:one_for_all` strategy: the supervisor will kill and restart all of its children processes whenever any one of them dies. This is a completely reasonable approach for our application, since the registry can't work without the bucket supervisor, and the bucket supervisor should terminate without the registry. Let's reimplement `init/1` in `KV.Supervisor` to encode those properties:
```elixir
def init(:ok) do
children = [
{DynamicSupervisor, name: KV.BucketSupervisor, strategy: :one_for_one},
{KV.Registry, name: KV.Registry}
]
Supervisor.init(children, strategy: :one_for_all)
end
```
There are two topics left before we move on to the next chapter.
## Shared state in tests
So far we have been starting one registry per test to ensure they are isolated:
```elixir
setup do
registry = start_supervised!(KV.Registry)
%{registry: registry}
defp via(name), do: {:via, Registry, {KV, name}}
end
```
Since we have changed our registry to use `KV.BucketSupervisor`, our tests are now relying on this shared supervisor even though each test has its own registry. The question is: should we?
The code is relatively simple. First we changed `start/2` to also start a dynamic supervisor named `KV.BucketSupervisor`. Then, when implemented `KV.create_bucket/1` which receives a bucket and starts with using our registry and dynamic supervisor. And we also added `KV.lookup_bucket/1` that receives the same name and attempts to find its PID.
It depends. It is ok to rely on shared state as long as we depend only on a non-shared partition of this state. Although multiple registries may start buckets on the shared bucket supervisor, those buckets and registries are isolated from each other. We would only run into concurrency issues if we used a function like `DynamicSupervisor.count_children(KV.BucketSupervisor)` which would count all buckets from all registries, potentially giving different results when tests run concurrently.
To make sure it all works as expected, let's write a test. Open up `test/kv_test.exs` and add this:
Since we have relied only on a non-shared partition of the bucket supervisor so far, we don't need to worry about concurrency issues in our test suite. In case it ever becomes a problem, we can start a supervisor per test and pass it as an argument to the registry `start_link` function.
```elixir
defmodule KVTest do
use ExUnit.Case, async: true
test "creates and looks up buckets by any name" do
name = "a unique name that won't be shared"
assert is_nil(KV.lookup_bucket(name))
assert {:ok, bucket} = KV.create_bucket(name)
assert KV.lookup_bucket(name) == bucket
assert KV.create_bucket(name) == {:error, {:already_started, bucket}}
end
end
```
The test shows we are creating and locating buckets with any name, making sure we use a unique name to avoid conflicts between tests.
## The `start_supervised` test helper
Before we move on, let's do some clean up.
In `test/kv/bucket_test.exs`, we explicitly invoked `KV.Bucket.start_link/1` to start our buckets. However, we now know that we should avoid calling `start_link/1` directly and instead start processes as part of supervision trees.
In order to aid testing, `ExUnit` already starts a supervision tree per test and provides the `start_supervised` function to start processes within test-specific supervision tree. One advantage of this approach is that `ExUnit` guarantees any started process is shut down at the end of the test too. Let's rewrite our tests to use it instead:
```elixir
defmodule KV.BucketTest do
use ExUnit.Case, async: true
test "stores values by key" do
{:ok, bucket} = start_supervised(KV.Bucket)
assert KV.Bucket.get(bucket, "milk") == nil
KV.Bucket.put(bucket, "milk", 3)
assert KV.Bucket.get(bucket, "milk") == 3
end
test "stores values by key on a named process", config do
{:ok, _} = start_supervised({KV.Bucket, name: config.test})
assert KV.Bucket.get(config.test, "milk") == nil
KV.Bucket.put(config.test, "milk", 3)
assert KV.Bucket.get(config.test, "milk") == 3
end
end
```
It is a small change, but our tests are now using all of the relevant best practices. Excellent!
## Observer
@@ -174,13 +205,10 @@ iex> :observer.start()
> When running `iex` inside a project with `iex -S mix`, `observer` won't be available as a dependency. To do so, you will need to call the following functions before:
>
> ```elixir
> iex> Mix.ensure_application!(:wx) # Not necessary on Erlang/OTP 27+
> iex> Mix.ensure_application!(:runtime_tools) # Not necessary on Erlang/OTP 27+
> iex> Mix.ensure_application!(:observer)
> iex> :observer.start()
> ```
>
> If any of the calls above fail, here is what may have happened: some package managers default to installing a minimized Erlang without WX bindings for GUI support. In some package managers, you may be able to replace the headless Erlang with a more complete package (look for packages named `erlang` vs `erlang-nox` on Debian/Ubuntu/Arch). In others managers, you may need to install a separate `erlang-wx` (or similarly named) package.
> If the call above fails, here is what may have happened: some package managers default to installing a minimized Erlang without WX bindings for GUI support. In some package managers, you may be able to replace the headless Erlang with a more complete package (look for packages named `erlang` vs `erlang-nox` on Debian/Ubuntu/Arch). In others managers, you may need to install a separate `erlang-wx` (or similarly named) package.
>
> There are conversations to improve this experience in future releases.
@@ -193,12 +221,12 @@ In the Applications tab, you will see all applications currently running in your
Not only that, as you create new buckets on the terminal, you should see new processes spawned in the supervision tree shown in Observer:
```elixir
iex> KV.Registry.create(KV.Registry, "shopping")
:ok
iex> KV.lookup_bucket("shopping")
#PID<0.89.0>
```
We will leave it up to you to further explore what Observer provides. Note you can double-click any process in the supervision tree to retrieve more information about it, as well as right-click a process to send "a kill signal", a perfect way to emulate failures and see if your supervisor reacts as expected.
At the end of the day, tools like Observer are one of the reasons you want to always start processes inside supervision trees, even if they are temporary, to ensure they are always reachable and introspectable.
Now that our buckets are properly linked and supervised, let's see how we can speed things up.
Now that our buckets are named and supervised, we are ready to start our server and start receiving requests.
+268 -228
View File
@@ -3,46 +3,70 @@
SPDX-FileCopyrightText: 2021 The Elixir Team
-->
# Client-server communication with GenServer
# Client-server with GenServer
In the [previous chapter](agents.md), we used agents to represent our buckets. In the [introduction to mix](introduction-to-mix.md), we specified we would like to name each bucket so we can do the following:
To wrap up our distributed key-value store, we will implement a feature where a client can subscribe to a bucket and receive realtime notifications of any modification happening in the bucket, regardless of where in the cluster the bucket is located.
```elixir
CREATE shopping
OK
We will do by adding a new command, called SUBSCRIBE, to be used like this:
PUT shopping milk 1
OK
GET shopping milk
1
OK
```text
SUBSCRIBE shopping
milk SET TO 1
eggs SET TO 10
milk DELETED
```
In the session above we interacted with the "shopping" bucket.
To make this work, we must change our `KV.Bucket` implementation to track subscriptions and emit broadcasts. However, as we will see, we cannot implement such on top of agents, and we will need to rewrite our bucket implementation to a `GenServer`.
Since agents are processes, each bucket has a process identifier (PID), but buckets do not have a name. Back [in the Process chapter](../getting-started/processes.md), we have learned that we can register processes in Elixir by giving them atom names:
## Links and monitors
Processes in Elixir are isolated. When they need to communicate, they do so by sending messages. However, how do you know when a process terminates, either because it has completed or due to a crash?
We have two options: links and monitors.
We have used links extensively. Whenever we started a process, we typically did so by using `start_link` or similar. The idea behind links is that, if any of the processes crash, the other will crash due to the link. We talked about them in the [Process chapter of the Getting Started guide](../getting-started/processes.md). Here is a refresher:
```elixir
iex> Agent.start_link(fn -> %{} end, name: :shopping)
{:ok, #PID<0.43.0>}
iex> KV.Bucket.put(:shopping, "milk", 1)
iex> self()
#PID<0.115.0>
iex> spawn_link(fn -> :nothing_bad_will_happen end)
#PID<0.116.0>
iex> self()
#PID<0.115.0>
```
```elixir
iex> spawn_link(fn -> raise "oops" end)
#PID<0.117.0>
12:37:33.229 [error] Process #PID<0.117.0> raised an exception
Interactive Elixir (1.18.4) - press Ctrl+C to exit (type h() ENTER for help)
iex> self()
#PID<0.118.0>
```
The reason why we links are so pervasive is because when we start a process inside a supervisor, we want our process to crash if the supervisor terminates. On the other hand, we don't want the supervisor to crash when a child terminates, and therefore supervisors trap exits from links by calling `Process.flag(:trap_exit, true)`.
In other words, links create an intrinsic relationship between the processes. If we simply want to track when a process dies, without tying their exit signals to each other, a better solution is to use monitors. When a monitored process terminates, we receive a message in our inbox, regardless of the reason:
```elixir
iex> pid = spawn(fn -> Process.sleep(5000) end)
#PID<0.119.0>
iex> Process.monitor(pid)
#Reference<0.1076459149.2159017989.118674>
iex> flush()
:ok
# Wait five seconds
iex> flush()
{:DOWN, #Reference<0.1076459149.2159017989.118674>, :process, #PID<0.119.0>, :normal}
:ok
iex> KV.Bucket.get(:shopping, "milk")
1
```
However, naming dynamic processes with atoms is a terrible idea! If we use atoms, we would need to convert the bucket name (often received from an external client) to atoms, and **we should never convert user input to atoms**. This is because atoms are not garbage collected. Once an atom is created, it is never reclaimed. Generating atoms from user input would mean the user can inject enough different names to exhaust our system memory!
Once the process terminates, we receive a "DOWN message", represented in a five-element tuple. The last element is the reason why it crashed (`:normal` means it terminated successfully).
In practice, it is more likely you will reach the Erlang VM limit for the maximum number of atoms before you run out of memory, which will bring your system down regardless.
Monitors will play a very important role in our subscribe feature. When a client subscribes to a bucket, the bucket will store the client PID and send messages to it on every change. However, if the client terminates (for example because it was disconnected), the bucket must remove the client from its list of subscribers (otherwise the list would keep on growing forever as clients connect and disconnect).
Instead of abusing the built-in name facility, we will create our own *process registry* that associates the bucket name to the bucket process.
The registry needs to guarantee that it is always up to date. For example, if one of the bucket processes crashes due to a bug, the registry must notice this change and avoid serving stale entries. In Elixir, we say the registry needs to *monitor* each bucket. Because our *registry* needs to be able to receive and handle ad-hoc messages from the system, the `Agent` API is not enough.
We will use a `GenServer` to create a registry process that can monitor the bucket processes. GenServer provides industrial strength functionality for building servers in both Elixir and OTP.
Please read the `GenServer` module documentation for an overview if you haven't yet. Once you do so, we are ready to proceed.
We chose the `Agent` module to implement our `KV.Bucket` and, unfortunately, agents cannot receive messages. So the first step is to rewrite our `KV.Bucket` to a `GenServer`. The `GenServer` module documentation has a good overview on what they are and how to implement them. Give it a read and then we are ready to proceed.
## GenServer callbacks
@@ -84,234 +108,255 @@ def handle_call({:put, key, value}, _from, state) do
end
```
There is quite a bit more ceremony in the GenServer code but, as we will see, it brings some benefits too.
For now, we will write only the server callbacks for our bucket registering logic, without providing a proper API, which we will do later.
Create a new file at `lib/kv/registry.ex` with the following contents:
Let's go ahead and rewrite `KV.Bucket` at once. Open up `lib/kv/bucket.ex` and replace its contents with this new version:
```elixir
defmodule KV.Registry do
defmodule KV.Bucket do
use GenServer
## Missing Client API - will add this later
@doc """
Starts a new bucket.
"""
def start_link(opts) do
GenServer.start_link(__MODULE__, %{}, opts)
end
## Defining GenServer Callbacks
@doc """
Gets a value from the `bucket` by `key`.
"""
def get(bucket, key) do
GenServer.call(bucket, {:get, key})
end
@doc """
Puts the `value` for the given `key` in the `bucket`.
"""
def put(bucket, key, value) do
GenServer.call(bucket, {:put, key, value})
end
@doc """
Deletes `key` from `bucket`.
Returns the current value of `key`, if `key` exists.
"""
def delete(bucket, key) do
GenServer.call(bucket, {:delete, key})
end
### Callbacks
@impl true
def init(:ok) do
{:ok, %{}}
def init(bucket) do
state = %{
bucket: bucket
}
{:ok, state}
end
@impl true
def handle_call({:lookup, name}, _from, names) do
{:reply, Map.fetch(names, name), names}
def handle_call({:get, key}, _from, state) do
value = get_in(state.bucket[key])
{:reply, value, state}
end
@impl true
def handle_cast({:create, name}, names) do
if Map.has_key?(names, name) do
{:noreply, names}
else
{:ok, bucket} = KV.Bucket.start_link([])
{:noreply, Map.put(names, name, bucket)}
end
def handle_call({:put, key, value}, _from, state) do
state = put_in(state.bucket[key], value)
{:reply, :ok, state}
end
def handle_call({:delete, key}, _from, state) do
{value, state} = pop_in(state.bucket[key])
{:reply, value, state}
end
end
```
There are two types of requests you can send to a GenServer: calls and casts. Calls are synchronous and the server **must** send a response back to such requests. While the server computes the response, the client is **waiting**. Casts are asynchronous: the server won't send a response back and therefore the client won't wait for one. Both requests are messages sent to the server, and will be handled in sequence. In the above implementation, we pattern-match on the `:create` messages, to be handled as cast, and on the `:lookup` messages, to be handled as call.
In order to invoke the callbacks above, we need to go through the corresponding `GenServer` functions. Let's start a registry, create a named bucket, and then look it up:
```elixir
iex> {:ok, registry} = GenServer.start_link(KV.Registry, :ok)
{:ok, #PID<0.136.0>}
iex> GenServer.cast(registry, {:create, "shopping"})
:ok
iex> {:ok, bucket} = GenServer.call(registry, {:lookup, "shopping"})
{:ok, #PID<0.174.0>}
```
Our `KV.Registry` process received a cast with `{:create, "shopping"}` and a call with `{:lookup, "shopping"}`, in this sequence. `GenServer.cast` will immediately return, as soon as the message is sent to the `registry`. The `GenServer.call` on the other hand, is where we would be waiting for an answer, provided by the above `KV.Registry.handle_call` callback.
You may also have noticed that we have added `@impl true` before each callback. The `@impl true` informs the compiler that our intention for the subsequent function definition is to define a callback. If by any chance we make a mistake in the function name or in the number of arguments, like we define a `handle_call/2`, the compiler would warn us there isn't any `handle_call/2` to define, and would give us the complete list of known callbacks for the `GenServer` module.
This is all good and well, but we still want to offer our users an API that allows us to hide our implementation details.
## The Client API
A GenServer is implemented in two parts: the client API and the server callbacks. You can either combine both parts into a single module or you can separate them into a client module and a server module. The client is any process that invokes the client function. The server is always the process identifier or process name that we will explicitly pass as argument to the client API. Here we'll use a single module for both the server callbacks and the client API.
Edit the file at `lib/kv/registry.ex`, filling in the blanks for the client API:
```elixir
## Client API
@doc """
Starts the registry.
"""
def start_link(opts) do
GenServer.start_link(__MODULE__, :ok, opts)
end
@doc """
Looks up the bucket pid for `name` stored in `server`.
Returns `{:ok, pid}` if the bucket exists, `:error` otherwise.
"""
def lookup(server, name) do
GenServer.call(server, {:lookup, name})
end
@doc """
Ensures there is a bucket associated with the given `name` in `server`.
"""
def create(server, name) do
GenServer.cast(server, {:create, name})
end
```
The first function is `start_link/1`, which starts a new GenServer passing a list of options. `start_link/1` calls out to `GenServer.start_link/3`, which takes three arguments:
The first function is `start_link/1`, which starts a new GenServer passing a list of options. `GenServer.start_link/3`, which takes three arguments:
1. The module where the server callbacks are implemented, in this case `__MODULE__` (meaning the current module)
2. The initialization arguments, in this case the atom `:ok`
2. The initialization arguments, in this case the empty bucket `%{}`
3. A list of options which can be used to specify things like the name of the server. For now, we forward the list of options that we receive on `start_link/1` to `GenServer.start_link/3`
3. A list of options which can be used to specify things like the name of the server. Once again, we forward the list of options that we receive on `start_link/1` to `GenServer.start_link/3`, as we did for agents
The next two functions, `lookup/2` and `create/2`, are responsible for sending these requests to the server. In this case, we have used `{:lookup, name}` and `{:create, name}` respectively. Requests are often specified as tuples, like this, in order to provide more than one "argument" in that first argument slot. It's common to specify the action being requested as the first element of a tuple, and arguments for that action in the remaining elements. Note that the requests must match the first argument to `handle_call/3` or `handle_cast/2`.
Once started, the GenServer will invoke the `init/1` callback, that receives the second argument given to `GenServer.start_link/3` and returns `{:ok, state}`, where state is a new map. We can already notice how the `GenServer` API makes the client/server segregation more apparent. `start_link/3` happens in the client, while `init/1` is the respective callback that runs on the server.
That's it for the client API. On the server side, we can implement a variety of callbacks to guarantee the server initialization, termination, and handling of requests. Those callbacks are optional and for now, we have only implemented the ones we care about. Let's recap.
There are two types of requests you can send to a GenServer: calls and casts. Calls are synchronous and the server **must** send a response back to such requests. While the server computes the response, the client is **waiting**. Casts are asynchronous: the server won't send a response back and therefore the client won't wait for one. Both requests are messages sent to the server, and will be handled in sequence. So far we have only used `GenServer.call/2`, to keep the same semantics as the Agent, but we will give `cast` a try when implementing subscriptions. Given we kept the same behaviour, all tests will still pass.
The first is the `init/1` callback, that receives the second argument given to `GenServer.start_link/3` and returns `{:ok, state}`, where state is a new map. We can already notice how the `GenServer` API makes the client/server segregation more apparent. `start_link/3` happens in the client, while `init/1` is the respective callback that runs on the server.
Each request must be implemented as a specific callback. For `call/2` requests, we implement a `handle_call/3` callback that receives the `request`, the process from which we received the request (`_from`), and the current server state (`state`). The `handle_call/3` callback returns a tuple in the format `{:reply, reply, updated_state}`. The first element of the tuple, `:reply`, indicates that the server should send a reply back to the client. The second element, `reply`, is what will be sent to the client while the third, `updated_state` is the new server state.
For `call/2` requests, we implement a `handle_call/3` callback that receives the `request`, the process from which we received the request (`_from`), and the current server state (`names`). The `handle_call/3` callback returns a tuple in the format `{:reply, reply, new_state}`. The first element of the tuple, `:reply`, indicates that the server should send a reply back to the client. The second element, `reply`, is what will be sent to the client while the third, `new_state` is the new server state.
Another Elixir feature we used in the implementation above are the nested traversal functions: `get_in/1`, `put_in/2`, and `pop_in/1`. Instead of keeping the `bucket` as our GenServer state, we defined a state map with a `bucket` key inside. This will be important as we also need to track subscribers as part of the GenServer state. These new functions make it straight-forward to manipulate data structures nested in other data structures.
For `cast/2` requests, we implement a `handle_cast/2` callback that receives the `request` and the current server state (`names`). The `handle_cast/2` callback returns a tuple in the format `{:noreply, new_state}`. Note that in a real application we would have probably implemented the callback for `:create` with a synchronous call instead of an asynchronous cast. We are doing it this way to illustrate how to implement a cast callback.
With our GenServer in place, let's work on subscription, starting with the tests.
There are other tuple formats both `handle_call/3` and `handle_cast/2` callbacks may return. There are other callbacks like `terminate/2` and `code_change/3` that we could implement. You are welcome to explore the full `GenServer` documentation to learn more about those.
## Implementing subscriptions
For now, let's write some tests to guarantee our GenServer works as expected.
Our new test will subscribe to a bucket and then assert that, as operations are performed against the bucket, we receive messages of said events.
## Testing a GenServer
Testing a GenServer is not much different from testing an agent. We will spawn the server on a setup callback and use it throughout our tests. Create a file at `test/kv/registry_test.exs` with the following:
Open up `test/kv/bucket_test.exs` and key this in:
```elixir
defmodule KV.RegistryTest do
use ExUnit.Case, async: true
test "subscribes to puts and deletes" do
{:ok, bucket} = start_supervised(KV.Bucket)
KV.Bucket.subscribe(bucket)
setup do
registry = start_supervised!(KV.Registry)
%{registry: registry}
KV.Bucket.put(bucket, "milk", 3)
assert_receive {:put, "milk", 3}
# Also check it works even from another process
spawn(fn -> KV.Bucket.delete(bucket, "milk") end)
assert_receive {:delete, "milk"}
end
```
In order to make the test pass, we need to implement the `KV.Bucket.subscribe/1`. So let's add these three new functions to `KV.Bucket`:
```elixir
@doc """
Subscribes the current process to the bucket.
"""
def subscribe(bucket) do
GenServer.cast(bucket, {:subscribe, self()})
end
test "spawns buckets", %{registry: registry} do
assert KV.Registry.lookup(registry, "shopping") == :error
KV.Registry.create(registry, "shopping")
assert {:ok, bucket} = KV.Registry.lookup(registry, "shopping")
KV.Bucket.put(bucket, "milk", 1)
assert KV.Bucket.get(bucket, "milk") == 1
@impl true
def handle_cast({:subscribe, pid}, state) do
Process.monitor(pid)
state = update_in(state.subscribers, &MapSet.put(&1, pid))
{:noreply, state}
end
end
```
Our test case first asserts there are no buckets in our registry, creates a named bucket, looks it up, and asserts it behaves as a bucket.
There is one important difference between the `setup` block we wrote for `KV.Registry` and the one we wrote for `KV.Bucket`. Instead of starting the registry by hand by calling `KV.Registry.start_link/1`, we instead called the `ExUnit.Callbacks.start_supervised!/2` function, passing the `KV.Registry` module.
The `start_supervised!` function was injected into our test module by `use ExUnit.Case`. It does the job of starting the `KV.Registry` process, by calling its `start_link/1` function. The advantage of using `start_supervised!` is that ExUnit will guarantee that the registry process will be shutdown **before** the next test starts. In other words, it helps guarantee that the state of one test is not going to interfere with the next one in case they depend on shared resources.
When starting processes during your tests, we should always prefer to use `start_supervised!`. We recommend you to change the `setup` block in `bucket_test.exs` to use `start_supervised!` too.
Run the tests and they should all pass!
## The need for monitoring
Everything we have done so far could have been implemented with a `Agent`. In this section, we will see one of many things that we can achieve with a GenServer that is not possible with an Agent.
Let's start with a test that describes how we want the registry to behave if a bucket stops or crashes:
```elixir
test "removes buckets on exit", %{registry: registry} do
KV.Registry.create(registry, "shopping")
{:ok, bucket} = KV.Registry.lookup(registry, "shopping")
Agent.stop(bucket)
assert KV.Registry.lookup(registry, "shopping") == :error
end
```
The test above will fail on the last assertion as the bucket name remains in the registry even after we stop the bucket process.
In order to fix this bug, we need the registry to monitor every bucket it spawns. Once we set up a monitor, the registry will receive a notification every time a bucket process exits, allowing us to clean the registry up.
Let's first play with monitors by starting a new console with `iex -S mix`:
```elixir
iex> {:ok, pid} = KV.Bucket.start_link([])
{:ok, #PID<0.66.0>}
iex> Process.monitor(pid)
#Reference<0.0.0.551>
iex> Agent.stop(pid)
:ok
iex> flush()
{:DOWN, #Reference<0.0.0.551>, :process, #PID<0.66.0>, :normal}
```
Note `Process.monitor(pid)` returns a unique reference that allows us to match upcoming messages to that monitoring reference. After we stop the agent, we can `flush/0` all messages and notice a `:DOWN` message arrived, with the exact reference returned by `monitor`, notifying that the bucket process exited with reason `:normal`.
Let's reimplement the server callbacks to fix the bug and make the test pass. First, we will modify the GenServer state to two maps: one that contains `name -> pid` and another that holds `ref -> name`. Then we need to monitor the buckets on `handle_cast/2` as well as implement a `handle_info/2` callback to handle the monitoring messages. The full server callbacks implementation is shown below:
```elixir
## Server callbacks
@impl true
def init(:ok) do
names = %{}
refs = %{}
{:ok, {names, refs}}
end
@impl true
def handle_call({:lookup, name}, _from, state) do
{names, _} = state
{:reply, Map.fetch(names, name), state}
end
@impl true
def handle_cast({:create, name}, {names, refs}) do
if Map.has_key?(names, name) do
{:noreply, {names, refs}}
else
{:ok, bucket} = KV.Bucket.start_link([])
ref = Process.monitor(bucket)
refs = Map.put(refs, ref, name)
names = Map.put(names, name, bucket)
{:noreply, {names, refs}}
@impl true
def handle_info({:DOWN, _ref, _type, pid, _reason}, state) do
state = update_in(state.subscribers, &MapSet.delete(&1, pid))
{:noreply, state}
end
end
@impl true
def handle_info({:DOWN, ref, :process, _pid, _reason}, {names, refs}) do
{name, refs} = Map.pop(refs, ref)
names = Map.delete(names, name)
{:noreply, {names, refs}}
end
@impl true
def handle_info(msg, state) do
require Logger
Logger.debug("Unexpected message in KV.Registry: #{inspect(msg)}")
{:noreply, state}
end
```
Observe that we were able to considerably change the server implementation without changing any of the client API. That's one of the benefits of explicitly segregating the server and the client.
On subscription, we send a `cast/2` request with the current process identifier and implement its `handle_cast/2` callback that receives the `request` and the current server state. We then proceed to monitor the given `pid` and add it to the list of subscribers, which we are implementing using `MapSet`. The `handle_cast/2` callback returns a tuple in the format `{:noreply, updated_state}`. Note that in a real application we would have probably implemented it with a synchronous call, as it provides back pressure, instead of an asynchronous cast. We are doing it this way to illustrate how to implement a cast callback.
Finally, different from the other callbacks, we have defined a "catch-all" clause for `handle_info/2` that discards and logs any unknown message. To understand why, let's move on to the next section.
Then, because we have monitored a process, once that process terminates, we will receive a "DOWN message". GenServers handle regular messages using the `handle_info/2` callback, which also typically return `{:noreply, updated_state}`. In this callback, we remove the PID that terminated from our list of subscribers.
We are almost there. We can see both `handle_cast/2` and `handle_info/2` callbacks assume there is a subscribers key in our state with a `MapSet`. So let's add it by updating the existing `init/1` to the following:
```elixir
@impl true
def init(bucket) do
state = %{
bucket: bucket,
subscribers: MapSet.new()
}
{:ok, state}
end
```
And finally let's update the callbacks for `put/3` and `delete/2` to broadcast messages whenever they are invoked, like this:
```elixir
def handle_call({:put, key, value}, _from, state) do
state = put_in(state.bucket[key], value)
broadcast(state, {:put, key, value})
{:reply, :ok, state}
end
def handle_call({:delete, key}, _from, state) do
{value, state} = pop_in(state.bucket[key])
broadcast(state, {:delete, key})
{:reply, value, state}
end
defp broadcast(state, message) do
for pid <- state.subscribers do
send(pid, message)
end
end
```
There is no need to modify the callback for `get/2`. And that's it, run the tests again, and our new test should pass!
## Wiring it all up
Now that our bucket deals with subscriptions, we need to expose this new functionality in our server. Let's once again start with the test.
Open up `test/kv/server_test.exs` and add this new test:
```elixir
test "subscribes to buckets", %{socket: socket, name: name} do
assert send_and_recv(socket, "CREATE #{name}\r\n") == "OK\r\n"
:gen_tcp.send(socket, "SUBSCRIBE #{name}\r\n")
{:ok, other} = :gen_tcp.connect(~c"localhost", 4040, @socket_options)
assert send_and_recv(other, "PUT #{name} milk 3\r\n") == "OK\r\n"
assert :gen_tcp.recv(socket, 0, 1000) == {:ok, "milk SET TO 3\r\n"}
assert send_and_recv(other, "DELETE #{name} milk\r\n") == "OK\r\n"
assert :gen_tcp.recv(socket, 0, 1000) == {:ok, "milk DELETED\r\n"}
end
```
The test creates a bucket and subscribes to it. Then it opens up another TCP connection to send commands. For each command sent, we expect the subscribed socket to receive a message.
To make the test pass, we need to change `KV.Command` to parse the new `SUBSCRIBE` command and then run it. Open up `lib/kv/commands.ex` and then first change the `parse/1` definition to the following:
```elixir
def parse(line) do
case String.split(line) do
["SUBSCRIBE", bucket] -> {:ok, {:subscribe, bucket}}
["CREATE", bucket] -> {:ok, {:create, bucket}}
["GET", bucket, key] -> {:ok, {:get, bucket, key}}
["PUT", bucket, key, value] -> {:ok, {:put, bucket, key, value}}
["DELETE", bucket, key] -> {:ok, {:delete, bucket, key}}
_ -> {:error, :unknown_command}
end
end
```
We added a new clause that converts "SUBSCRIBE" into a tuple. Now we need to match on this tuple within `run/1`. We can do so by adding a new clause at the bottom of `run/1`, with the following code:
```elixir
def run({:subscribe, bucket}, socket) do
lookup(bucket, fn pid ->
KV.Bucket.subscribe(pid)
:inet.setopts(socket, active: true)
receive_messages(socket)
end)
end
defp receive_messages(socket) do
receive do
{:put, key, value} ->
:gen_tcp.send(socket, "#{key} SET TO #{value}\r\n")
receive_messages(socket)
{:delete, key} ->
:gen_tcp.send(socket, "#{key} DELETED\r\n")
receive_messages(socket)
{:tcp_closed, ^socket} ->
{:error, :closed}
# If we receive any message, including socket writes, we discard them
_ ->
receive_messages(socket)
end
end
```
Let's go over it by parts. We use the existing `lookup/2` private function to lookup for a bucket. If one is found, we subscribe the current process to the bucket. Then we call `:inet.setopts(socket, active: true)` (which we will explain soon) and `receive_messages/1`.
`receive_messages/1` awaits for messages from the bucket and then calls itself again, becoming a loop. We match on `{:put, key, value}` and `{:delete, key}` and write to those events to the socket. We also match on `{:tcp_closed, ^socket}`, which is a message that will be delivered if the TCP socket closes, and use it to abort the loop. We discard any other message.
At this point you may be wondering: where does `{:tcp_closed, ^socket}` come from?
So far, when receiving messages from the socket, we used `:gen_tcp.recv/3` to perform calls that will block the current process until content is available. This is known as "passive mode". However, we can also ask `:gen_tcp` to stream messages to the current process inbox as they arrive, which is known as "active mode", which is exactly what we configured when we called `:inet.setopts(socket, active: true)`. Those messages have the shape `{:tcp, socket, data}`. When the socket is in active mode and it is closed, it delivers a `{:tcp_closed, socket}` message. Once we receive this message, we exit the loop, which will exit the connection process. Since the bucket is monitoring the process, it will automatically remove the subscription too. You could verify this in practice by adding a `COUNT SUBSCRIPTIONS` command that returns the number of subscribers for a given bucket.
In practice, many systems would prefer to call `:inet.setopts(socket, active: :once)` to specify only a single TCP message should be delivered to avoid overflowing message queues. Once the message is received, they call `:inet.setopts/2` again. In our case, we are simply discarding anything that arrives over the socket, so setting `active: true` is equally fine. In all scenarios, the benefit of using active mode is that the process can receive TCP messages as well as messages from other processes at the same time, instead of blocking on `:gen_tcp.recv/3`.
To wrap it all up, you should give our new feature a try in a distributed setting too. Start two `NODES=... PORT=... iex --sname ... -S mix` instances. In one of them, create a bucket. In the other, subscribe to the same bucket. Once you go back to the first shell, you will see that, even as you send commands to the bucket in one machine, the messages will be streamed to the other one. In other words, our subscription system is also distributed, and all we had to do is to send messages!
## `call`, `cast` or `info`?
@@ -319,25 +364,20 @@ So far we have used three callbacks: `handle_call/3`, `handle_cast/2` and `handl
1. `handle_call/3` must be used for synchronous requests. This should be the default choice as waiting for the server reply is a useful back-pressure mechanism.
2. `handle_cast/2` must be used for asynchronous requests, when you don't care about a reply. A cast does not guarantee the server has received the message and, for this reason, should be used sparingly. For example, the `create/2` function we have defined in this chapter should have used `call/2`. We have used `cast/2` for didactic purposes.
2. `handle_cast/2` must be used for asynchronous requests, when you don't care about a reply. A cast does not guarantee the server has received the message and, for this reason, should be used sparingly. For example, the `subscribe/1` function we have defined in this chapter should have used `call/2`. We have used `cast/2` for educational purposes.
3. `handle_info/2` must be used for all other messages a server may receive that are not sent via `GenServer.call/2` or `GenServer.cast/2`, including regular messages sent with `send/2`. The monitoring `:DOWN` messages are an example of this.
Since any message, including the ones sent via `send/2`, go to `handle_info/2`, there is a chance that unexpected messages will arrive to the server. Therefore, if we don't define the catch-all clause, those messages could cause our registry to crash, because no clause would match. We don't need to worry about such cases for `handle_call/3` and `handle_cast/2` though. Calls and casts are only done via the `GenServer` API, so an unknown message is quite likely a developer mistake.
To help developers remember the differences between call, cast and info, the supported return values and more, we have a tiny [GenServer cheat sheet](https://elixir-lang.org/downloads/cheatsheets/gen-server.pdf).
## Monitors or links?
## Agents or GenServers?
We have previously learned about links in the [Process chapter](../getting-started/processes.md). Now, with the registry complete, you may be wondering: when should we use monitors and when should we use links?
Before moving forward to the last chapter, you may be wondering: in the future, should you use an `Agent` or a `GenServer`?
Links are bi-directional. If you link two processes and one of them crashes, the other side will crash too (unless it is trapping exits). A monitor is uni-directional: only the monitoring process will receive notifications about the monitored one. In other words: use links when you want linked crashes, and monitors when you just want to be informed of crashes, exits, and so on.
As we saw throughout this guide, agents are straight-forward to get started but they are limited in what they can do. Agents are effectively a subset of GenServers. In fact, agents are implemented on top of GenServers. As well as supervisors, the `Registry` module, and many other features you will find in both Erlang and Elixir.
Returning to our `handle_cast/2` implementation, you can see the registry is both linking and monitoring the buckets:
In other words, GenServers are the most essential component for building concurrent and fault-tolerant systems in Elixir. They provide a robust and flexible framework for managing state and coordinating interactions between processes.
```elixir
{:ok, bucket} = KV.Bucket.start_link([])
ref = Process.monitor(bucket)
```
For those reasons, many adopt a rule of thumb to never use Agents and jump straight into GenServers instead. On the other hand, others are more than fine with using agents to store a bit of state here and there. Either way, you will be fine!
This is a bad idea, as we don't want the registry to crash when a bucket crashes. The proper fix is to actually not link the bucket to the registry. Instead, we will link each bucket to a special type of process called Supervisors, which are explicitly designed to handle failures and crashes. We will learn more about them in the next chapter.
This is the last feature we have implemented for our distributed key-value store. In the next chapter, we will learn how to package our application before shipping it to production.
@@ -9,8 +9,8 @@ In this guide, we will build a complete Elixir application, with its own supervi
The requirements for this guide are (see `elixir -v`):
* Elixir 1.15.0 onwards
* Erlang/OTP 24 onwards
* Elixir 1.18.0 onwards
* Erlang/OTP 27 onwards
The application works as a distributed key-value store. We are going to organize key-value pairs into buckets and distribute those buckets across multiple nodes. We will also build a simple client that allows us to connect to any of those nodes and send requests such as:
@@ -44,7 +44,7 @@ In this chapter, we will create our first project using Mix and explore differen
> #### Source code {: .info}
>
> The final code for the application built in this guide is in [this repository](https://github.com/josevalim/kv_umbrella) and can be used as a reference.
> The final code for the application built in this guide is in [this repository](https://github.com/josevalim/kv) and can be used as a reference.
> #### Is this guide required reading? {: .info}
>
@@ -82,7 +82,7 @@ Let's take a brief look at those generated files.
> #### Executables in the `PATH` {: .info}
>
> Mix is an Elixir executable. This means that in order to run `mix`, you need to have both `mix` and `elixir` executables in your PATH. That's what happens when you install Elixir.
> Mix is an Elixir executable. This means that in order to run `mix`, you need to have both `mix` and `elixir` executables in your [`PATH`](https://en.wikipedia.org/wiki/PATH_(variable)). That's what happens when you install Elixir.
## Project compilation
+170
View File
@@ -0,0 +1,170 @@
<!--
SPDX-License-Identifier: Apache-2.0
SPDX-FileCopyrightText: 2021 The Elixir Team
-->
# Releases
Now that our application is ready, you may be wondering how we can package our application to run in production. After all, all of our code so far depends on Erlang and Elixir versions that are installed in your current system. To achieve this goal, Elixir provides releases.
A release is a self-contained directory that consists of your application code, all of its dependencies, plus the whole Erlang Virtual Machine (VM) and runtime. Once a release is assembled, it can be packaged and deployed to a target as long as the target runs on the same operating system (OS) distribution and version as the machine that assembled the release.
To get started, simply run `mix release` while setting `MIX_ENV=prod`:
```console
$ MIX_ENV=prod mix release
Compiling 4 files (.ex)
Generated kv app
* assembling kv-0.1.0 on MIX_ENV=prod
* using config/runtime.exs to configure the release at runtime
Release created at _build/prod/rel/kv
# To start your system
_build/prod/rel/kv/bin/kv start
Once the release is running:
# To connect to it remotely
_build/prod/rel/kv/bin/kv remote
# To stop it gracefully (you may also send SIGINT/SIGTERM)
_build/prod/rel/kv/bin/kv stop
To list all commands:
_build/prod/rel/kv/bin/kv
```
Excellent! A release was assembled in `_build/prod/rel/kv`. Everything you need to run your application is inside that directory. In particular, there is a `bin/kv` file which is the entry point to your system. It supports multiple commands, such as:
* `bin/kv start`, `bin/kv start_iex`, `bin/kv restart`, and `bin/kv stop` — for general management of the release
* `bin/kv rpc COMMAND` and `bin/kv remote` — for running commands on the running system or to connect to the running system
* `bin/kv eval COMMAND` — to start a fresh system that runs a single command and then shuts down
* `bin/kv daemon` and `bin/kv daemon_iex` — to start the system as a daemon on Unix-like systems
* `bin/kv install` — to install the system as a service on Windows machines
If you run `bin/kv start_iex` inside the release directory, it will start the system using a short name (`--sname`) equal to the release name, which in this case is `kv`. The next step is to start two instances, on different ports and different names, as we did earlier on. But before we do this, let's talk a bit about the benefits of releases.
## Why releases?
Releases allow developers to precompile and package all of their code and the runtime into a single unit. The benefits of releases are:
* Code preloading. The VM has two mechanisms for loading code: interactive and embedded. By default, it runs in the interactive mode which dynamically loads modules when they are used for the first time. The first time your application calls `Enum.map/2`, the VM will find the `Enum` module and load it. There's a downside. When you start a new server in production, it may need to load many other modules, causing the first requests to have an unusual spike in response time. Releases run in embedded mode, which loads all available modules upfront, guaranteeing your system is ready to handle requests after booting.
* Configuration and customization. Releases give developers fine grained control over system configuration and the VM flags used to start the system.
* Self-contained. A release does not require the source code to be included in your production artifacts. All of the code is precompiled and packaged. Releases do not even require Erlang or Elixir on your servers, as they include the Erlang VM and its runtime by default. Furthermore, both Erlang and Elixir standard libraries are stripped to bring only the parts you are actually using.
* Multiple releases. You can assemble different releases with different configuration per application or even with different applications altogether.
We have written extensive documentation on releases, so [please check the official documentation for more information](`mix release`). For now, we will continue exploring some of the features outlined above.
## Configuring releases
Releases also provide built-in hooks for configuring almost every need of the production system:
* `config/config.exs` — provides build-time application configuration, which is executed before our application compiles. This file often imports configuration files based on the environment, such as `config/dev.exs` and `config/prod.exs`.
* `config/runtime.exs` — provides runtime application configuration. It is executed every time the release boots and is further extensible via config providers.
* `rel/env.sh.eex` and `rel/env.bat.eex` — template files that are copied into every release and executed on every command to set up environment variables, including ones specific to the VM, and the general environment.
* `rel/vm.args.eex` — a template file that is copied into every release and provides static configuration of the Erlang Virtual Machine and other runtime flags.
In this case, we already have specified a `config/runtime.exs` that deals with both `PORT` and `NODES` environment variables. Furthermore, while releases don't accept a `--sname` parameter, they do allow us to set the name via the `RELEASE_NODE` env var. Therefore, we can start two copies of the system by jumping into `_build/prod/rel/kv` and typing this (remember to adjust `@computer-name` to your actual computer name):
```console
$ NODES="foo@computer-name,bar@computer-name" PORT=4040 RELEASE_NODE="foo" bin/kv start_iex
```
```console
$ NODES="foo@computer-name,bar@computer-name" PORT=4041 RELEASE_NODE="bar" bin/kv start_iex
```
To verify it all worked out, you can type `Node.list` in the IEx section and see if it returns the other node. If it doesn't, you can start diagnosing, first by comparing the node names within each `iex>` prompt and calling `Node.connect/1` directly. With applications running, you can `telnet` into them as usual too.
While the above is enough to get started, you may want to perform advanced configuration based on the environment you are replying to. Releases provide scripts for that, which are great to automate based on host, network, or cloud settings.
## Operating System scripts
Every release contains an environment file, named `env.sh` on Unix-like systems and `env.bat` on Windows machines, that executes before the Elixir system starts. In this file, you can execute any OS-level code, such as invoke other applications, set environment variables and so on. Some of those environment variables can even configure how the release itself runs.
For instance, releases run using short-names (`--sname`). However, if you want to actually run a distributed key-value store in production, you will need multiple nodes and start the release with the `--name` option. We can achieve this by setting the `RELEASE_DISTRIBUTION` environment variable inside the `env.sh` and `env.bat` files. Mix already has a template for said files which we can customize, so let's ask Mix to copy them to our application:
$ mix release.init
* creating rel/vm.args.eex
* creating rel/remote.vm.args.eex
* creating rel/env.sh.eex
* creating rel/env.bat.eex
If you open up `rel/env.sh.eex`, you will see:
```shell
#!/bin/sh
# # Sets and enables heart (recommended only in daemon mode)
# case $RELEASE_COMMAND in
# daemon*)
# HEART_COMMAND="$RELEASE_ROOT/bin/$RELEASE_NAME $RELEASE_COMMAND"
# export HEART_COMMAND
# export ELIXIR_ERL_OPTIONS="-heart"
# ;;
# *)
# ;;
# esac
# # Set the release to load code on demand (interactive) instead of preloading (embedded).
# export RELEASE_MODE=interactive
# # Set the release to work across nodes.
# # RELEASE_DISTRIBUTION must be "sname" (local), "name" (distributed) or "none".
# export RELEASE_DISTRIBUTION=name
# export RELEASE_NODE=<%= @release.name %>
```
The steps necessary to work across nodes is already commented out as an example. You can enable full distribution by setting the `RELEASE_DISTRIBUTION` variable to `name`.
If you are on Windows, you will have to open up `rel/env.bat.eex`, where you will find this:
```bat
@echo off
rem Set the release to load code on demand (interactive) instead of preloading (embedded).
rem set RELEASE_MODE=interactive
rem Set the release to work across nodes.
rem RELEASE_DISTRIBUTION must be "sname" (local), "name" (distributed) or "none".
rem set RELEASE_DISTRIBUTION=name
rem set RELEASE_NODE=<%= @release.name %>
```
Once again, set the `RELEASE_DISTRIBUTION` variable to `name` and you are good to go!
## VM arguments
The `rel/vm.args.eex` allows you to specify low-level flags that control how the Erlang VM and its runtime operate. You specify entries as if you were specifying arguments in the command line with code comments also supported. Here is the default generated file:
## Customize flags given to the VM: https://www.erlang.org/doc/man/erl.html
## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here
## Increase number of concurrent ports/sockets
##+Q 65536
## Tweak GC to run more often
##-env ERL_FULLSWEEP_AFTER 10
You can see [a complete list of VM arguments and flags in the Erlang documentation](http://www.erlang.org/doc/man/erl.html).
## Summing up
Throughout the guide, we have built a very simple distributed key-value store as an opportunity to explore many constructs like generic servers, supervisors, tasks, agents, applications and more. Not only that, we have written tests for the whole application, got familiar with ExUnit, and learned how to use the Mix build tool to accomplish a wide range of tasks.
If you are looking for a distributed key-value store to use in production, you should definitely look into [Riak](http://riak.com/products/riak-kv/), which also runs in the Erlang VM. In Riak, the buckets are replicated and stored across several nodes to avoid data loss.
Of course, Elixir can be used for much more than distributed key-value stores. Embedded systems, data-processing and data-ingestion, web applications, audio/video streaming systems, machine learning, and others are many of the different domains Elixir excels at. We hope this guide has prepared you to explore any of those domains or any future domain you may desire to bring Elixir into.
Happy coding!
@@ -3,142 +3,65 @@
SPDX-FileCopyrightText: 2021 The Elixir Team
-->
# Supervision trees and applications
# Registries and supervision trees
In the previous chapter about `GenServer`, we implemented `KV.Registry` to manage buckets. At some point, we started monitoring buckets so we were able to take action whenever a `KV.Bucket` crashed. Although the change was relatively small, it introduced a question which is frequently asked by Elixir developers: what happens when something fails?
In the [previous chapter](agents.md), we used agents to represent our buckets. In the [introduction to mix](introduction-to-mix.md), we specified we would like to name each bucket so we can do the following:
Before we added monitoring, if a bucket crashed, the registry would forever point to a bucket that no longer exists. If a user tried to read or write to the crashed bucket, it would fail. Any attempt at creating a new bucket with the same name would just return the PID of the crashed bucket. In other words, that registry entry for that bucket would forever be in a bad state. Once we added monitoring, the registry automatically removes the entry for the crashed bucket. Trying to lookup the crashed bucket now (correctly) says the bucket does not exist and a user of the system can successfully create a new one if desired.
```text
CREATE shopping
OK
In practice, we are not expecting the processes working as buckets to fail. But, if it does happen, for whatever reason, we can rest assured that our system will continue to work as intended.
PUT shopping milk 1
OK
If you have prior programming experience, you may be wondering: "could we just guarantee the bucket does not crash in the first place?". As we will see, Elixir developers tend to refer to those practices as "defensive programming". That's because a live production system has dozens of different reasons why something can go wrong. The disk can fail, memory can be corrupted, bugs, the network may stop working for a second, etc. If we were to write software that attempted to protect or circumvent all of those errors, we would spend more time handling failures than writing our own software!
Therefore, an Elixir developer prefers to "let it crash" or "fail fast". And one of the most common ways we can recover from a failure is by restarting whatever part of the system crashed.
For example, imagine your computer, router, printer, or whatever device is not working properly. How often do you fix it by restarting it? Once we restart the device, we reset the device back to its initial state, which is well-tested and guaranteed to work. In Elixir, we apply this same approach to software: whenever a process crashes, we start a new process to perform the same job as the crashed process.
In Elixir, this is done by a Supervisor. A Supervisor is a process that supervises other processes and restarts them whenever they crash. To do so, Supervisors manage the whole life cycle of any supervised processes, including startup and shutdown.
In this chapter, we will learn how to put those concepts into practice by supervising the `KV.Registry` process. After all, if something goes wrong with the registry, the whole registry is lost and no bucket could ever be found! To address this, we will define a `KV.Supervisor` module that guarantees that our `KV.Registry` is up and running at any given moment.
At the end of the chapter, we will also talk about Applications. As we will see, Mix has been packaging all of our code into an application, and we will learn how to customize our application to guarantee that our Supervisor and the Registry are up and running whenever our system starts.
## Our first supervisor
A supervisor is a process which supervises other processes, which we refer to as child processes. The act of supervising a process includes three distinct responsibilities. The first one is to start child processes. Once a child process is running, the supervisor may restart a child process, either because it terminated abnormally or because a certain condition was reached. For example, a supervisor may restart all children if any child dies. Finally, a supervisor is also responsible for shutting down the child processes when the system is shutting down. Please see the `Supervisor` module for a more in-depth discussion.
Creating a supervisor is not much different from creating a GenServer. We are going to define a module named `KV.Supervisor`, which will use the Supervisor behaviour, inside the `lib/kv/supervisor.ex` file:
```elixir
defmodule KV.Supervisor do
use Supervisor
def start_link(opts) do
Supervisor.start_link(__MODULE__, :ok, opts)
end
@impl true
def init(:ok) do
children = [
KV.Registry
]
Supervisor.init(children, strategy: :one_for_one)
end
end
GET shopping milk
1
OK
```
Our supervisor has a single child so far: `KV.Registry`. After we define a list of children, we call `Supervisor.init/2`, passing the children and the supervision strategy.
In the example session above we interacted with the "shopping" bucket by referencing its name. Therefore, an important feature in our key-value store is to give names to processes.
The supervision strategy dictates what happens when one of the children crashes. `:one_for_one` means that if a child dies, it will be the only one restarted. Since we have only one child now, that's all we need. The `Supervisor` behaviour supports several strategies, which we will discuss in this chapter.
Once the supervisor starts, it will traverse the list of children and it will invoke the `child_spec/1` function on each module.
The `child_spec/1` function returns the child specification which describes how to start the process, if the process is a worker or a supervisor, if the process is temporary, transient or permanent and so on. The `child_spec/1` function is automatically defined when we `use Agent`, `use GenServer`, `use Supervisor`, etc. Let's give it a try in the terminal with `iex -S mix`:
We have also learned in the previous chapter we can already name our buckets. For example:
```elixir
iex> KV.Registry.child_spec([])
%{id: KV.Registry, start: {KV.Registry, :start_link, [[]]}}
```
We will learn those details as we move forward on this guide. If you would rather peek ahead, check the `Supervisor` docs.
After the supervisor retrieves all child specifications, it proceeds to start its children one by one, in the order they were defined, using the information in the `:start` key in the child specification. For our current specification, it will call `KV.Registry.start_link([])`.
Let's take the supervisor for a spin:
```elixir
iex> {:ok, sup} = KV.Supervisor.start_link([])
{:ok, #PID<0.148.0>}
iex> Supervisor.which_children(sup)
[{KV.Registry, #PID<0.150.0>, :worker, [KV.Registry]}]
```
So far we have started the supervisor and listed its children. Once the supervisor started, it also started all of its children.
What happens if we intentionally crash the registry started by the supervisor? Let's do so by sending it a bad input on `call`:
```elixir
iex> [{_, registry, _, _}] = Supervisor.which_children(sup)
[{KV.Registry, #PID<0.150.0>, :worker, [KV.Registry]}]
iex> GenServer.call(registry, :bad_input)
08:52:57.311 [error] GenServer #PID<0.150.0> terminating
** (FunctionClauseError) no function clause matching in KV.Registry.handle_call/3
iex> Supervisor.which_children(sup)
[{KV.Registry, #PID<0.157.0>, :worker, [KV.Registry]}]
```
Notice how the supervisor automatically started a new registry, with a new PID, in place of the first one once we caused it to crash due to a bad input.
In the previous chapters, we have always started processes directly. For example, we would call `KV.Registry.start_link([])`, which would return `{:ok, pid}`, and that would allow us to interact with the registry via its `pid`. Now that processes are started by the supervisor, we have to directly ask the supervisor who its children are, and fetch the PID from the returned list of children. In practice, doing so every time would be very expensive. To address this, we often give names to processes, allowing them to be uniquely identified in a single machine from anywhere in our code.
Let's learn how to do that.
## Naming processes
While our application will have many buckets, it will only have a single registry. Therefore, whenever we start the registry, we want to give it a unique name so we can reach out to it from anywhere. We do so by passing a `:name` option to `KV.Registry.start_link/1`.
Let's slightly change our children definition (in `KV.Supervisor.init/1`) to be a list of tuples instead of a list of atoms:
```elixir
def init(:ok) do
children = [
{KV.Registry, name: KV.Registry}
]
```
With this in place, the supervisor will now start `KV.Registry` by calling `KV.Registry.start_link(name: KV.Registry)`.
If you revisit the `KV.Registry.start_link/1` implementation, you will remember it simply passes the options to GenServer:
```elixir
def start_link(opts) do
GenServer.start_link(__MODULE__, :ok, opts)
end
```
which in turn will register the process with the given name. The `:name` option expects an atom for locally named processes (locally named means it is available to this machine — there are other options, which we won't discuss here). Since module identifiers are atoms (try `i(KV.Registry)` in IEx), we can name a process after the module that implements it, provided there is only one process for that name. This helps when debugging and introspecting the system.
Let's give the updated supervisor a try inside `iex -S mix`:
```elixir
iex> KV.Supervisor.start_link([])
{:ok, #PID<0.66.0>}
iex> KV.Registry.create(KV.Registry, "shopping")
iex> KV.Bucket.start_link(name: :shopping)
{:ok, #PID<0.43.0>}
iex> KV.Bucket.put(:shopping, "milk", 1)
:ok
iex> KV.Registry.lookup(KV.Registry, "shopping")
{:ok, #PID<0.70.0>}
iex> KV.Bucket.get(:shopping, "milk")
1
```
This time the supervisor started a named registry, allowing us to create buckets without having to explicitly fetch the PID from the supervisor. You should also know how to make the registry crash again, without looking up its PID: give it a try.
However, naming dynamic processes with atoms is a terrible idea! If we use atoms, we would need to convert the bucket name (often received from an external client) to atoms, and **we should never convert user input to atoms**. This is because atoms are not garbage collected. Once an atom is created, it is never reclaimed. Generating atoms from user input would mean the user can inject enough different names to exhaust our system memory!
> At this point, you may be wondering: should you also locally name bucket processes? Remember buckets are started dynamically based on user input. Since local names MUST be atoms, we would have to dynamically create atoms, which is a bad idea since once an atom is defined, it is never erased nor garbage collected. This means that, if we create atoms dynamically based on user input, we will eventually run out of memory (or to be more precise, the VM will crash because it imposes a hard limit on the number of atoms). This limitation is precisely why we created our own registry (or why one would use Elixir's built-in `Registry` module).
In practice, it is more likely you will reach the Erlang VM limit for the maximum number of atoms before you run out of memory, which will bring your system down regardless.
We are getting closer and closer to a fully working system. The supervisor automatically starts the registry. But how can we automatically start the supervisor whenever our system starts? To answer this question, let's talk about applications.
Luckily, Elixir (and Erlang) comes with built-in abstractions for naming processes, called name registries, each with different trade-offs which we will explore throughout these guides.
## Local, decentralized, and scalable registry
Elixir ships with a single-node process registry module aptly called `Registry`. Its main feature is that you can use any Elixir value to name a process, not only atoms. Let's take it for a spin in `iex`:
```elixir
iex> Registry.start_link(name: KV, keys: :unique)
iex> name = {:via, Registry, {KV, "shopping"}}
iex> KV.Bucket.start_link(name: name)
{:ok, #PID<0.43.0>}
iex> KV.Bucket.put(name, "milk", 1)
:ok
iex> KV.Bucket.get(name, "milk")
1
```
As you can see, instead of passing an atom to the `:name` option, we pass a tuple of shape `{:via, registry_module, {registry_name, process_name}}`, and everything just worked. You could have used anything as the `process_name`, even an integer or a map! That's because all of Elixir built-in behaviours, agents, supervisors, tasks, etc, are compatible with naming registries, as long as you pass them using the "via" tuple format.
Therefore, all we need to do to name our buckets is to start a `Registry`, using `Registry.start_link/1`. But you may be wondering, where exactly should we place that?
## Understanding applications
We have been working inside an application this entire time. Every time we changed a file and ran `mix compile`, we could see a `Generated kv app` message in the compilation output.
Every Elixir project is an application. Elixir itself is defined in an application named `:elixir`. The `ExUnit.Case` module is part of the `:ex_unit` application. And so forth.
In fact, we have been working inside an application this entire time. Every time we changed a file and ran `mix compile`, we could see a `Generated kv app` message in the compilation output.
We can find the generated `.app` file at `_build/dev/lib/kv/ebin/kv.app`. Let's have a look at its contents:
@@ -146,8 +69,7 @@ We can find the generated `.app` file at `_build/dev/lib/kv/ebin/kv.app`. Let's
{application,kv,
[{applications,[kernel,stdlib,elixir,logger]},
{description,"kv"},
{modules,['Elixir.KV','Elixir.KV.Bucket','Elixir.KV.Registry',
'Elixir.KV.Supervisor']},
{modules,['Elixir.KV','Elixir.KV.Bucket']},
{registered,[]},
{vsn,"0.1.0"}]}.
```
@@ -156,7 +78,7 @@ This file contains Erlang terms (written using Erlang syntax). Even though we ar
> The `logger` application ships as part of Elixir. We stated that our application needs it by specifying it in the `:extra_applications` list in `mix.exs`. See the [official documentation](`Logger`) for more information.
In a nutshell, an application consists of all the modules defined in the `.app` file, including the `.app` file itself. An application has generally only two directories: `ebin`, for Elixir artifacts, such as `.beam` and `.app` files, and `priv`, with any other artifact or asset you may need in your application.
In a nutshell, an application consists of all the modules defined in the `.app` file, including the `.app` file itself. The application itself is located at the `_build/dev/lib/kv` folder and typically has only two directories: `ebin`, for Elixir artifacts, such as `.beam` and `.app` files, and `priv`, with any other artifact or asset you may need in your application.
Although Mix generates and maintains the `.app` file for us, we can customize its contents by adding new entries to the `application/0` function inside the `mix.exs` project file. We are going to do our first customization soon.
@@ -196,9 +118,9 @@ iex> Application.ensure_all_started(:kv)
{:ok, [:logger, :kv]}
```
In practice, our tools always start our applications for us, but there is an API available if you need fine-grained control.
In practice, our tools always start our applications for us, and you don't have to worry about the above, but it is good to know how it all works behind the scenes.
## The application callback
### The application callback
Whenever we invoke `iex -S mix`, Mix automatically starts our application by calling `Application.start(:kv)`. But can we customize what happens when our application starts? As a matter of fact, we can! To do so, we define an application callback.
@@ -213,50 +135,119 @@ The first step is to tell our application definition (for example, our `.app` fi
end
```
The `:mod` option specifies the "application callback module", followed by the arguments to be passed on application start. The application callback module can be any module that implements the `Application` behaviour.
The `:mod` option specifies the "application callback module", followed by the arguments to be passed on application start. The application callback module can be any module that invokes `use Application`. Since we have specified `KV` as the module callback, let's change the `KV` module defined in `lib/kv.ex` to the following:
To implement the `Application` behaviour, we have to `use Application` and define a `start/2` function. The goal of `start/2` is to start a supervisor, which will then start any child services or execute any other code our application may need. Let's use this opportunity to start the `KV.Supervisor` we have implemented earlier in this chapter.
```elixir
defmodule KV do
use Application
end
```
Since we have specified `KV` as the module callback, let's change the `KV` module defined in `lib/kv.ex` to implement a `start/2` function:
Now run `mix test` and you will see a couple things happening. First of all, you will get a compilation warning:
```text
Compiling 1 file (.ex)
warning: function start/2 required by behaviour Application is not implemented (in module KV)
│
1 │ defmodule KV do
│ ~~~~~~~~~~~~~~~
│
└─ lib/kv.ex:1: KV (module)
```
This warning is telling us that `use Application` actually defines a behaviour, which expects us to implement to a `start/2` function in our `KV` module.
Then our application does not even boot because the `start/2` function is not actually implemented:
```text
18:29:39.109 [notice] Application kv exited: exited in: KV.start(:normal, [])
** (EXIT) an exception was raised:
** (UndefinedFunctionError) function KV.start/2 is undefined or private
```
Implementing the `start/2` callback is relatively straight-forward, all we need to do is to start a supervision tree, and return `{:ok, root_supervisor_pid}`. The `Supervisor.start_link/2` function does precisely that, it only expects a list of children and the supervision strategy. Let's just pass an empty list of children for now:
```elixir
defmodule KV do
use Application
# The @impl true annotation says we are implementing a callback
@impl true
def start(_type, _args) do
# Although we don't use the supervisor name below directly,
# it can be useful when debugging or introspecting the system.
KV.Supervisor.start_link(name: KV.Supervisor)
Supervisor.start_link([], strategy: :one_for_one)
end
end
```
> Please note that by doing this, we are breaking the boilerplate test case which tested the `hello` function in `KV`. You can simply remove that test case.
Now run `mix test` again and our app should boot but we should see one failure. When we changed the `KV` module, we broke the boilerplate test case which tested the `KV.hello/0` function. You can simply remove that test case and we are back to a green suite.
When we `use Application`, we may define a couple of functions, similar to when we used `Supervisor` or `GenServer`. This time we only had to define a `start/2` function. The `Application` behaviour also has a `stop/1` callback, but it is rarely used in practice. You can check the documentation for more information.
We wrote very little code but we did something incredibly powerful. We now have a function, `KV.start/2` that is invoked whenever your application starts. This gives us the perfect place to start our key-value registry. The `Application` module also allows us to define a `stop/1` callback and other functionality. You can check the `Application` and `Supervisor` modules for extensive documentation on their uses.
Now that you have defined an application callback which starts our supervisor, we expect the `KV.Registry` process to be up and running as soon as we start `iex -S mix`. Let's give it another try:
Let's finally start our registry.
## Supervision trees
Now that we have the `start/2` callback, we can finally go ahead and start our registry. You may be tempted to do it like this:
```elixir
iex> KV.Registry.create(KV.Registry, "shopping")
:ok
iex> KV.Registry.lookup(KV.Registry, "shopping")
{:ok, #PID<0.88.0>}
def start(_type, _args) do
Registry.start_link(name: KV, keys: :unique)
Supervisor.start_link([], strategy: :one_for_one)
end
```
Let's recap what is happening. Whenever we invoke `iex -S mix`, it automatically starts our application by calling `Application.start(:kv)`, which then invokes the application callback. The application callback's job is to start a **supervision tree**. Right now, our supervisor has a single child named `KV.Registry`, started with name `KV.Registry`. Our supervisor could have other children, and some of these children could be their own supervisors with their own children, leading to the so-called supervision trees.
However, this would not be a good idea. In Elixir, we typically start processes inside supervision trees. In fact, we rarely use the `start_link` functions to start processes (except at the root of the supervision tree itself). Instead, do this:
```elixir
def start(_type, _args) do
children = [
{Registry, name: KV, keys: :unique}
]
Supervisor.start_link(children, strategy: :one_for_one)
end
```
A supervisor receives one or more child specifications that tell it exactly how to start each child. A child specification is typically represented by a `{module, options}` pair, as shown above, and often as simply the module name. Sometimes, these children are supervisors themselves, giving us supervision trees.
Let's take it for a spin and see if we can indeed name our buckets using our new registry. Let's make sure to start a new `iex -S mix` (`recompile()` is not enough, as it does not reload your supervision tree) and then:
```iex
iex> name = {:via, Registry, {KV, "shopping"}}
iex> KV.Bucket.start_link(name: name)
{:ok, #PID<0.43.0>}
iex> KV.Bucket.put(name, "milk", 1)
:ok
iex> KV.Bucket.get(name, "milk")
1
```
Perfect, this time we didn't need to start the registry inside `iex`, as it was started as part of the application itself.
By starting processes inside supervisors, we gain important properties such as:
* **Introspection**: for each application, you can fully introspect and visualize each process in its supervision tree, its memory usage, message queue, etc
* **Resilience**: when a process fails for an unexpected reason, its supervisor controls if and how those processes should be restarted, leading to self-healing systems
* **Graceful shutdown**: when your application is shutting down, the children of a supervision tree are terminated in the opposite order they were started, leading to graceful shutdowns
## Projects or applications?
Mix makes a distinction between projects and applications. Based on the contents of our `mix.exs` file, we would say we have a Mix project that defines the `:kv` application. As we will see in later chapters, there are projects that don't define any application.
Mix makes a distinction between projects and applications. Based on the contents of our `mix.exs` file, we would say we have a Mix project that defines the `:kv` application.
When we say "project" you should think about Mix. Mix is the tool that manages your project. It knows how to compile your project, test your project and more. It also knows how to compile and start the application relevant to your project.
When we talk about applications, we talk about OTP. Applications are the entities that are started and stopped as a whole by the runtime. You can learn more about applications and how they relate to booting and shutting down of your system as a whole in the documentation for the `Application` module.
## Next steps
## Summing up
Although this chapter was the first time we implemented a supervisor, it was not the first time we used one! In the previous chapter, when we used `start_supervised!` to start the registry during our tests, `ExUnit` started the registry under a supervisor managed by the ExUnit framework itself. By defining our own supervisor, we provide more structure on how we initialize, shutdown and supervise processes in our applications, aligning our production code and tests with best practices.
We learned important concepts in this chapter:
But we are not done yet. So far we are supervising the registry but our application is also starting buckets. Since buckets are started dynamically, we can use a special type of supervisor called `DynamicSupervisor`, which is optimized to handle such scenarios. Let's explore it next.
* Naming registries allow us to find processes in a given machine (or, as we will see in the future, even in a cluster)
* Applications bundle our modules, its dependencies, and how code starts and stops
* Processes are started as part of supervisors for introspection and fault-tolerance
In the next chapter, we will tie it all up by making sure all our buckets are named and supervised. To do so, we will learn a new tool called dynamic supervisors.
@@ -5,7 +5,7 @@
# Task and gen_tcp
In this chapter, we are going to learn how to use Erlang's [`:gen_tcp` module](`:gen_tcp`) to serve requests. This provides a great opportunity to explore Elixir's `Task` module. In future chapters, we will expand our server so that it can actually serve the commands.
In this chapter, we are going to learn how to use Erlang's [`:gen_tcp` module](`:gen_tcp`) to serve requests. This provides a great opportunity to explore Elixir's `Task` module. In future chapters, we will expand our server so that it can actually interact with buckets.
## Echo server
@@ -17,10 +17,10 @@ A TCP server, in broad strokes, performs the following steps:
2. Waits for a client connection on that port and accepts it
3. Reads the client request and writes a response back
Let's implement those steps. Move to the `apps/kv_server` application, open up `lib/kv_server.ex`, and add the following functions:
Let's implement those steps. Create a new `lib/kv/server.ex` and add the following functions:
```elixir
defmodule KVServer do
defmodule KV.Server do
require Logger
def accept(port) do
@@ -62,7 +62,7 @@ defmodule KVServer do
end
```
We are going to start our server by calling `KVServer.accept(4040)`, where 4040 is the port. The first step in `accept/1` is to listen to the port until the socket becomes available and then call `loop_acceptor/1`. `loop_acceptor/1` is a loop accepting client connections. For each accepted connection, we call `serve/1`.
We are going to start our server by calling `KV.Server.accept(4040)`, where 4040 is the port. The first step in `accept/1` is to listen to the port until the socket becomes available and then call `loop_acceptor/1`. `loop_acceptor/1` is a loop accepting client connections. For each accepted connection, we call `serve/1`.
`serve/1` is another loop that reads a line from the socket and writes those lines back to the socket. Note that the `serve/1` function uses the pipe operator `|>/2` to express this flow of operations. The pipe operator evaluates the left side and passes its result as the first argument to the function on the right side. The example above:
@@ -85,7 +85,7 @@ This is pretty much all we need to implement our echo server. Let's give it a tr
Start an IEx session inside the `kv_server` application with `iex -S mix`. Inside IEx, run:
```elixir
iex> KVServer.accept(4040)
iex> KV.Server.accept(4040)
```
The server is now running, and you will even notice the console is blocked. Let's use [a `telnet` client](https://en.wikipedia.org/wiki/Telnet) to access our server. There are clients available on most operating systems, and their command lines are generally similar:
@@ -109,10 +109,12 @@ My particular telnet client can be exited by typing `ctrl + ]`, typing `quit`, a
Once you exit the telnet client, you will likely see an error in the IEx session:
** (MatchError) no match of right hand side value: {:error, :closed}
(kv_server) lib/kv_server.ex:45: KVServer.read_line/1
(kv_server) lib/kv_server.ex:37: KVServer.serve/1
(kv_server) lib/kv_server.ex:30: KVServer.loop_acceptor/1
```text
** (MatchError) no match of right hand side value: {:error, :closed}
(kv) lib/kv/server.ex:45: KV.Server.read_line/1
(kv) lib/kv/server.ex:37: KV.Server.serve/1
(kv) lib/kv/server.ex:30: KV.Server.loop_acceptor/1
```
That's because we were expecting data from `:gen_tcp.recv/2` but the client closed the connection. We need to handle such cases better in future revisions of our server.
@@ -120,36 +122,25 @@ For now, there is a more important bug we need to fix: what happens if our TCP a
## Tasks
We have learned about agents, generic servers, and supervisors. They are all meant to work with multiple messages or manage state. But what do we use when we only need to execute some task and that is it?
Whenever you have an existing function and you simply want to execute it when your application starts, the `Task` module is exactly you need. For example, it has a `Task.start_link/1` function that receives an anonymous function and executes it inside a new process that will be part of a supervision tree.
The `Task` module provides this functionality exactly. For example, it has a `Task.start_link/1` function that receives an anonymous function and executes it inside a new process that will be part of a supervision tree.
Let's give it a try. Open up `lib/kv_server/application.ex`, and let's change the supervisor in the `start/2` function to the following:
Let's give it a try. Open up `lib/kv.ex` and let's add a new child:
```elixir
def start(_type, _args) do
children = [
{Task, fn -> KVServer.accept(4040) end}
{Registry, name: KV, keys: :unique},
{DynamicSupervisor, name: KV.BucketSupervisor, strategy: :one_for_one},
{Task, fn -> KV.Server.accept(4040) end}
]
opts = [strategy: :one_for_one, name: KVServer.Supervisor]
Supervisor.start_link(children, opts)
Supervisor.start_link(children, strategy: :one_for_one)
end
```
As usual, we've passed a two-element tuple as a child specification, which in turn will invoke `Task.start_link/1`.
With this change, we are saying that we want to run `KV.Server.accept(4040)` as a task. We are hardcoding the port for now but we will make this a configuration in later chapters. As usual, we've passed a two-element tuple as a child specification, which in turn will invoke `Task.start_link/1`.
With this change, we are saying that we want to run `KVServer.accept(4040)` as a task. We are hardcoding the port for now but this could be changed in a few ways, for example, by reading the port out of the system environment when starting the application:
```elixir
port = String.to_integer(System.get_env("PORT") || "4040")
# ...
{Task, fn -> KVServer.accept(port) end}
```
Insert these changes in your code and now you may start your application using the following command `PORT=4321 mix run --no-halt`, notice how we are passing the port as a variable, but still defaults to 4040 if none is given.
Now that the server is part of the supervision tree, it should start automatically when we run the application. Start your server, now passing the port, and once again use the `telnet` client to make sure that everything still works:
Now that the server is part of the supervision tree, it should start automatically when we run the application. Run `iex -S mix` to boot the app and use the `telnet` client to make sure that everything still works:
```console
$ telnet 127.0.0.1 4321
@@ -178,7 +169,7 @@ HELLOOOOOO?
It doesn't seem to work at all. That's because we are serving requests in the same process that are accepting connections. When one client is connected, we can't accept another client.
## Task supervisor
## Adding (flawed) concurrency
In order to make our server handle simultaneous connections, we need to have one process working as an acceptor that spawns other processes to serve requests. One solution would be to change:
@@ -195,43 +186,49 @@ to also use `Task.start_link/1`:
```elixir
defp loop_acceptor(socket) do
{:ok, client} = :gen_tcp.accept(socket)
Task.start_link(fn -> serve(client) end)
{:ok, pid} = Task.start_link(fn -> serve(client) end)
:ok = :gen_tcp.controlling_process(client, pid)
loop_acceptor(socket)
end
```
We are starting a linked Task directly from the acceptor process. But we've already made this mistake once. Do you remember?
In the new acceptor loop, we are starting a new task every time there is a new client. Now, if you attempt to connect two clients at the same time, it should work!
This is similar to the mistake we made when we called `KV.Bucket.start_link/1` straight from the registry. That meant a failure in any bucket would bring the whole registry down.
Or does it? For example, what happens when you exit one telnet session? The other session should crash! The reason of this crash is two fold:
The code above would have the same flaw: if we link the `serve(client)` task to the acceptor, a crash when serving a request would bring the acceptor, and consequently all other connections, down.
1. We have a bug in our server where we don't expect `:gen_tcp.recv/2` to return an `{:error, :closed}` tuple
We fixed the issue for the registry by using a simple one for one supervisor. We are going to use the same tactic here, except that this pattern is so common with tasks that `Task` already comes with a solution: a simple one for one supervisor that starts temporary tasks as part of our supervision tree.
2. Because each server task is linked to the acceptor process, if one task crashes, the acceptor process will also crash, taking down all other tasks and clients
Let's change `start/2` once again, to add a supervisor to our tree:
An important rule thumb throughout this guide is to always start processes as children of supervisors. The code above is an excellent example of what happens when we don't. If we don't isolate the different parts of our systems, failures can now cascade through our system, as it would happen in other languages.
To fix this, we could use a `DynamicSupervisor`, but tasks also provide a specialized `Task.Supervisor` which has better ergonomics and is optimized for supervising tasks themselves. Let's give it a try.
## Adding a task supervisor
Let's change `start/2` in `lib/kv.ex` once more, to add the task supervisor to our tree:
```elixir
def start(_type, _args) do
port = String.to_integer(System.get_env("PORT") || "4040")
children = [
{Task.Supervisor, name: KVServer.TaskSupervisor},
{Task, fn -> KVServer.accept(port) end}
{Registry, name: KV, keys: :unique},
{DynamicSupervisor, name: KV.BucketSupervisor, strategy: :one_for_one},
{Task.Supervisor, name: KV.ServerSupervisor},
{Task, fn -> KV.Server.accept(4040) end}
]
opts = [strategy: :one_for_one, name: KVServer.Supervisor]
Supervisor.start_link(children, opts)
Supervisor.start_link(children, strategy: :one_for_one)
end
```
We'll now start a `Task.Supervisor` process with name `KVServer.TaskSupervisor`. Remember, since the acceptor task depends on this supervisor, the supervisor must be started first.
We'll now start a `Task.Supervisor` process with name `KV.TaskSupervisor`. Keep in mind that the order children are started matters. For example, the acceptor must come last because, if it comes first, it means our application can start accepting requests before the `Task.Supervisor` is running or before we can locate buckets. Shutting down an application will also stop the children in reverse order, guaranteeing a clean termination.
Now we need to change `loop_acceptor/1` to use `Task.Supervisor` to serve each request:
```elixir
defp loop_acceptor(socket) do
{:ok, client} = :gen_tcp.accept(socket)
{:ok, pid} = Task.Supervisor.start_child(KVServer.TaskSupervisor, fn -> serve(client) end)
{:ok, pid} = Task.Supervisor.start_child(KV.BucketSupervisor, fn -> serve(client) end)
:ok = :gen_tcp.controlling_process(client, pid)
loop_acceptor(socket)
end
@@ -239,76 +236,72 @@ end
You might notice that we added a line, `:ok = :gen_tcp.controlling_process(client, pid)`. This makes the child process the "controlling process" of the `client` socket. If we didn't do this, the acceptor would bring down all the clients if it crashed because sockets would be tied to the process that accepted them (which is the default behavior).
Start a new server with `PORT=4040 mix run --no-halt` and we can now open up many concurrent telnet clients. You will also notice that quitting a client does not bring the acceptor down. Excellent!
Now start a new server with `iex -S mix` and try to open up many concurrent telnet clients. You will notice that quitting a client does not bring the acceptor down, even though we haven't fixed the bug in `:gen_tcp.recv/2` yet (which we will address in the next chapter). Excellent!
Here is the full echo server implementation:
## Restart strategies
There is one important topic we haven't explored yet with the necessary depth. What happens when a supervised process crashes?
In the previous chapter, when we started a bucket and killed it, the supervisor automatically started one in its place:
```elixir
defmodule KVServer do
require Logger
@doc """
Starts accepting connections on the given `port`.
"""
def accept(port) do
{:ok, socket} = :gen_tcp.listen(port,
[:binary, packet: :line, active: false, reuseaddr: true])
Logger.info "Accepting connections on port #{port}"
loop_acceptor(socket)
end
defp loop_acceptor(socket) do
{:ok, client} = :gen_tcp.accept(socket)
{:ok, pid} = Task.Supervisor.start_child(KVServer.TaskSupervisor, fn -> serve(client) end)
:ok = :gen_tcp.controlling_process(client, pid)
loop_acceptor(socket)
end
defp serve(socket) do
socket
|> read_line()
|> write_line(socket)
serve(socket)
end
defp read_line(socket) do
{:ok, data} = :gen_tcp.recv(socket, 0)
data
end
defp write_line(line, socket) do
:gen_tcp.send(socket, line)
end
end
iex> children = [{KV.Bucket, name: :shopping}]
iex> Supervisor.start_link(children, strategy: :one_for_one)
iex> KV.Bucket.put(:shopping, "milk", 1)
iex> pid = Process.whereis(:shopping)
#PID<0.48.0>
iex> Process.exit(pid, :kill)
true
iex> Process.whereis(:shopping)
#PID<0.50.0>
```
Since we have changed the supervisor specification, we need to ask: is our supervision strategy still correct?
What exactly happens when a process terminates is part of its child specification. For `KV.Bucket`, we have this:
In this case, the answer is yes: if the acceptor crashes, there is no need to crash the existing connections. On the other hand, if the task supervisor crashes, there is no need to crash the acceptor too.
```elixir
iex> KV.Bucket.child_spec([])
%{id: KV.Bucket, start: {KV.Bucket, :start_link, [[]]}}
```
However, there is still one concern left, which are the restart strategies. Tasks, by default, have the `:restart` value set to `:temporary`, which means they are not restarted. This is an excellent default for the connections started via the `Task.Supervisor`, as it makes no sense to restart a failed connection, but it is a bad choice for the acceptor. If the acceptor crashes, we want to bring the acceptor up and running again.
However, for tasks, we have this:
Let's fix this. We know that for a child of shape `{Task, fun}`, Elixir will invoke `Task.child_spec(fun)` to retrieve the underlying child specification. Therefore, one might imagine that to change the `{Task, fun}` specification to have a `:restart` of `:permanent`, we would need to change the `Task` module. However, that's impossible to do, as the `Task` module is defined as part of Elixir's standard library (and even if it was possible, it is unlikely it would be a good idea).
Luckily, this can be done by using `Supervisor.child_spec/2`, which allows us to configure a child specification with new values. Let's rewrite `start/2` in `KVServer.Application` once more:
```elixir
iex> Task.child_spec(fn -> :ok end)
%{
id: Task,
restart: :temporary,
start: {Task, :start_link, [#Function<43.39164016/0 in :erl_eval.expr/6>]}
}
```
Notice that a task says `:restart` is `:temporary`. `KV.Bucket` says nothing, which means it defaults to `:permanent`. `:temporary` means that a process is never restarted, regardless of why it crashed. `:permanent` means a process is always restarted, regardless of the exit reason. There is also `:transient`, which means it won't be restarted as long as it terminates successfully.
Now we must ask ourselves, are those the correct settings?
For `KV.Bucket`, using `:permanent` seem logical, as should not request the user to recreate a bucket they have previous created. Although currently we would lose the bucket data, in actual system we would add mechanisms to recover it on initialization. However, for tasks, we have used them in two opposing ways in this chapter, which means at least one of them is wrong.
We use a task to start the acceptor. The acceptor is a critical component of our infrastructure. If it crashes, it means we won't accept further requests, and our server would then be useless as no one can connect to it. On the other hand, we also use `Task.Supervisor` to start tasks that deal with each connection. In this case, restarting may not be useful at all, given the reason we crashed could just as well be a connection issue, and attempting to restart over the same connection would lead to further failures.
Therefore, we want the acceptor to actually run in `:permanent` mode, while we preserve the `Task.Supervisor` as `:temporary`. Luckily Elixir has an API that allows us to change an existing child specification, which we use below.
Let's change `start/2` in `lib/kv.ex` once more to the following:
```elixir
def start(_type, _args) do
port = String.to_integer(System.get_env("PORT") || "4040")
children = [
{Task.Supervisor, name: KVServer.TaskSupervisor},
Supervisor.child_spec({Task, fn -> KVServer.accept(port) end}, restart: :permanent)
{Registry, name: KV, keys: :unique},
{DynamicSupervisor, name: KV.BucketSupervisor, strategy: :one_for_one},
{Task.Supervisor, name: KV.ServerSupervisor},
Supervisor.child_spec({Task, fn -> KV.Server.accept(4040) end}, restart: :permanent)
]
opts = [strategy: :one_for_one, name: KVServer.Supervisor]
Supervisor.start_link(children, opts)
Supervisor.start_link(children, strategy: :one_for_one)
end
```
Now we have an always running acceptor that starts temporary task processes under an always running task supervisor.
## Wrapping up
## Leveraging the ecosystem
In this chapter, we implemented a basic TCP acceptor while exploring concurrency and fault-tolerance. Our acceptor can manage concurrent connections, but it is still not ready for production. Production-ready TCP servers run a pool of acceptors, each with their own supervisor. Elixir's `PartitionSupervisor` might be used to partition and scale the acceptor, but it is out of scope for this guide. In practice, you will use existing packages tailored for this use-case, such as [Ranch](https://github.com/ninenines/ranch) (in Erlang) or [Thousand Island](https://github.com/mtrudel/thousand_island) (in Elixir).
@@ -14,12 +14,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
Elixir version | Support
:------------- | :-----------------------------
1.19 | Development
1.18 | Bug fixes and security patches
1.19 | Bug fixes and security patches
1.18 | Security patches only
1.17 | Security patches only
1.16 | Security patches only
1.15 | Security patches only
1.14 | Security patches only
New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). All security releases [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date).
@@ -35,7 +34,7 @@ Although we expect the vast majority of programs to remain compatible over time,
* Bugs: if an API has undesired behavior, a program that depends on the buggy behavior may break if the bug is fixed. We reserve the right to fix such bugs.
* Compiler front-end: improvements may be done to the compiler, introducing new warnings for ambiguous modes and providing more detailed error messages. Those can lead to compilation errors (when running with `--warning-as-errors`) or tooling failures when asserting on specific error messages (although one should avoid such). We reserve the right to do such improvements.
* Compiler front-end: improvements may be done to the compiler, introducing new warnings for ambiguous modes and providing more detailed error messages. Those can lead to compilation errors (when running with `--warnings-as-errors`) or tooling failures when asserting on specific error messages (although one should avoid such). We reserve the right to do such improvements.
* Imports: new functions may be added to the `Kernel` module, which is auto-imported. They may collide with local functions defined in your modules. Collisions can be resolved in a backwards compatible fashion using `import Kernel, except: [...]` with a list of all functions you don't want to be imported from `Kernel`. We reserve the right to do such additions.
@@ -49,7 +48,7 @@ Erlang/OTP versioning is independent from the versioning of Elixir. Erlang relea
Elixir version | Supported Erlang/OTP versions
:------------- | :-------------------------------
1.19 | 26 - 27
1.19 | 26 - 28
1.18 | 25 - 27
1.17 | 25 - 27
1.16 | 24 - 26
@@ -236,4 +235,4 @@ Version | Deprecated feature | Replaced by (ava
[v1.16]: https://github.com/elixir-lang/elixir/blob/v1.16/CHANGELOG.md#4-hard-deprecations
[v1.17]: https://github.com/elixir-lang/elixir/blob/v1.17/CHANGELOG.md#4-hard-deprecations
[v1.18]: https://github.com/elixir-lang/elixir/blob/v1.18/CHANGELOG.md#4-hard-deprecations
[v1.19]: https://github.com/elixir-lang/elixir/blob/main/CHANGELOG.md#4-hard-deprecations
[v1.19]: https://github.com/elixir-lang/elixir/blob/v1.19/CHANGELOG.md#4-hard-deprecations
@@ -97,7 +97,7 @@ If you give it an integer, it negates it. If you give it a boolean, it negates i
We can say this function has the type `(integer() -> integer())` because it is capable of receiving an integer and returning an integer. In this case, `(integer() -> integer())` is a set that represents all functions that can receive an integer and return an integer. Even though this function can receive other arguments and return other values, it is still part of the `(integer() -> integer())` set.
This function also has the type `(boolean() -> boolean())`, because it receives booleans and returns booleans. Therefore, we can say the overall type of the function is `(integer() -> integer()) and (boolean() -> boolean())`. The intersection means the function belongs to both sets.
This function also has the type `(boolean() -> boolean())`, because it also receives booleans and returns booleans. If you pass the function above to another function that expects `(boolean() -> boolean())`, type checking will succeed. Therefore, we can say the overall type of the function is `(integer() -> integer()) and (boolean() -> boolean())`. The intersection means the function belongs to both sets.
At this point, you may ask, why not a union? As a real-world example, take a t-shirt with green and yellow stripes. We can say the t-shirt belongs to the set of "t-shirts with green color". We can also say the t-shirt belongs to the set of "t-shirts with yellow color". Let's see the difference between unions and intersections:
@@ -105,7 +105,7 @@ At this point, you may ask, why not a union? As a real-world example, take a t-s
* `(t_shirts_with_green() and t_shirts_with_yellow())` - contains t-shirts with both green and yellow (and maybe other colors)
Since the t-shirt has both colors, we say it belongs to the intersection of both sets. The same way that a function that goes from `(integer() -> integer())` and `(boolean() -> boolean())` is also an intersection. In practice, it does not make sense to define the union of two functions in Elixir, so the compiler will always point to the right direction.
Since the t-shirt has both colors, we could say it belongs to the union of green and yellow t-shirts, but doing so would not capture the fact it is both green and yellow. Therefore it is more precise to say it belongs to the intersection of both sets. The same way that a function that goes from `(integer() -> integer())` and `(boolean() -> boolean())` is also an intersection. In practice, it is not useful to define the union of two functions in Elixir, so the compiler will point you to the right direction if you specify the wrong one.
## The `dynamic()` type
@@ -142,11 +142,13 @@ Inferring type signatures comes with a series of trade-offs:
* Cascading errors - when a user accidentally makes type errors or the code has conflicting assumptions, type inference may lead to less clear error messages as the type system tries to reconcile diverging type assumptions across code paths.
On the other hand, type inference offers the benefit of enabling type checking for functions and codebases without requiring the user to add type annotations. To balance these trade-offs, Elixir aims to provide "module type inference": our goal is to infer the types of functions considering the current module, Elixir's standard library and your dependencies (in the future). Calls to modules within the same project are assumed to be `dynamic()` as to reduce cyclic dependencies and the need for recompilations. Once types are inferred, then the whole project is type checked considering all modules and all types (inferred or otherwise).
On the other hand, type inference offers the benefit of enabling type checking for functions and codebases without requiring the user to add type annotations. To balance these trade-offs, we are exploring "module type inference": our goal is to infer type signatures considering invocations of functions in the same module and of functions from *other applications* (such as Elixir itself and your dependencies). Once module types are inferred, your whole project is type checked considering all declared and inferred types.
Type inference in Elixir is best-effort: it doesn't guarantee it will find all possible type incompatibilities, only that it may find bugs where all combinations of a type _will_ fail, even in the absence of explicit type annotations. It is meant to be an efficient routine that brings developers some benefits of static typing without requiring any effort from them.
We have successfully implemented these features as part of Elixir v1.19, by performing inference of all constructs (except guards), taking into account the signatures from calls to functions within the same module and in Elixir's standard library.
In the long term, Elixir developers who want typing guarantees must explicitly add type signatures to their functions (see "Roadmap"). Any function with an explicit type signature will be typed checked against the user-provided annotations, as in other statically typed languages, without performing type inference. In summary, type checking will rely on type signatures and only fallback to inferred types when no signature is available.
In future releases, we plan to perform type inference of guards and also consider the type signatures of your dependencies during inference. Overall, our goal with type inference is to find bugs where all combinations of a type _will_ fail, even in the absence of explicit type annotations. It is meant to be an efficient routine that brings developers some benefits of static typing without requiring any effort from them.
Keep in mind this only applies to *type inference*. Once we introduce type signatures and you explicitly annotate your functions, type inference and the trade-offs above no longer play a role. Any function with an explicit type signature will be typed checked against the user-provided annotations, as in other statically typed languages.
## Roadmap

Some files were not shown because too many files have changed in this diff Show More