sezaru

sezaru

I have this attribute in my resource:

    attribute :status, :atom do
      allow_nil? false
      public? true

      constraints one_of: [:draft, :open, :pending, :sold, :inactive]

      default :draft
    end

And I have a read action that I call via AshGraphql to retrieve data from that resource.

When I try to filter that call by the status attribute, I’m getting an error.

Here is my graphql query:

query {
  listValidProperties(
    filter: {
      status: {in: ["OPEN", "PENDING", "SOLD"]}
    }
    limit: 5
    offset: 0
  ) {
    results {
      id
    }
  }
}

I get the following error in the terminal:

[warning] `96e7c8cc-071b-4623-a8a3-cad0b578f02f`: AshGraphql.Error not implemented for error:

** (Ash.Error.Query.InvalidFilterValue) Invalid filter value `status in ["OPEN", "PENDING", "SOLD"]`: No matching types. Possible types: [[:any, {:array, :same}]]
    (elixir 1.17.2) lib/process.ex:864: Process.info/2
    (ash 3.4.8) lib/ash/error/query/invalid_filter_value.ex:5: Ash.Error.Query.InvalidFilterValue.exception/1
    (ash 3.4.8) lib/ash/query/operator/operator.ex:184: Ash.Query.Operator.try_cast_with_ref/3
    (ash 3.4.8) lib/ash/filter/filter.ex:4015: anonymous fn/5 in Ash.Filter.parse_predicates/3
    (elixir 1.17.2) lib/enum.ex:4858: Enumerable.List.reduce/3
    (elixir 1.17.2) lib/enum.ex:2585: Enum.reduce_while/3
    (ash 3.4.8) lib/ash/filter/filter.ex:2764: Ash.Filter.add_expression_part/3
    (ash 3.4.8) lib/ash/filter/filter.ex:2902: anonymous fn/3 in Ash.Filter.add_expression_part/3
    (elixir 1.17.2) lib/enum.ex:4858: Enumerable.List.reduce/3
    (elixir 1.17.2) lib/enum.ex:2585: Enum.reduce_while/3
    (ash 3.4.8) lib/ash/filter/filter.ex:2901: Ash.Filter.add_expression_part/3
    (ash 3.4.8) lib/ash/filter/filter.ex:2460: anonymous fn/3 in Ash.Filter.parse_expression/2
    (elixir 1.17.2) lib/enum.ex:4858: Enumerable.List.reduce/3
    (elixir 1.17.2) lib/enum.ex:2585: Enum.reduce_while/3
    (ash 3.4.8) lib/ash/filter/filter.ex:334: Ash.Filter.parse/3
    (ash 3.4.8) lib/ash/query/query.ex:2699: Ash.Query.do_filter/3
    (stdlib 6.0.1) maps.erl:860: :maps.fold_1/4
    (ash_graphql 1.3.4) lib/graphql/resolver.ex:423: AshGraphql.Graphql.Resolver.resolve/2
    (absinthe 1.7.8) lib/absinthe/phase/document/execution/resolution.ex:234: Absinthe.Phase.Document.Execution.Resolution.reduce_resolution/1
    (absinthe 1.7.8) lib/absinthe/phase/document/execution/resolution.ex:189: Absinthe.Phase.Document.Execution.Resolution.do_resolve_field/3
    (absinthe 1.7.8) lib/absinthe/phase/document/execution/resolution.ex:174: Absinthe.Phase.Document.Execution.Resolution.do_resolve_fields/6
    (absinthe 1.7.8) lib/absinthe/phase/document/execution/resolution.ex:145: Absinthe.Phase.Document.Execution.Resolution.resolve_fields/4
    (absinthe 1.7.8) lib/absinthe/phase/document/execution/resolution.ex:88: Absinthe.Phase.Document.Execution.Resolution.walk_result/5
    (absinthe 1.7.8) lib/absinthe/phase/document/execution/resolution.ex:67: Absinthe.Phase.Document.Execution.Resolution.perform_resolution/3
    (absinthe 1.7.8) lib/absinthe/phase/document/execution/resolution.ex:24: Absinthe.Phase.Document.Execution.Resolution.resolve_current/3
    (absinthe 1.7.8) lib/absinthe/pipeline.ex:408: Absinthe.Pipeline.run_phase/3
    (absinthe_plug 1.5.8) lib/absinthe/plug.ex:536: Absinthe.Plug.run_query/4
    (absinthe_plug 1.5.8) lib/absinthe/plug.ex:290: Absinthe.Plug.call/2
    (phoenix 1.7.14) lib/phoenix/router/route.ex:42: Phoenix.Router.Route.call/2
    (phoenix 1.7.14) lib/phoenix/router.ex:484: Phoenix.Router.__call__/5
    (core 1.91.3) lib/core_web/endpoint.ex:1: CoreWeb.Endpoint.plug_builder_call/2
    (core 1.91.3) deps/plug/lib/plug/debugger.ex:136: CoreWeb.Endpoint."call (overridable 3)"/2
    (core 1.91.3) lib/core_web/endpoint.ex:1: CoreWeb.Endpoint."call (overridable 4)"/2
    (core 1.91.3) lib/core_web/endpoint.ex:1: CoreWeb.Endpoint.call/2
    (phoenix 1.7.14) lib/phoenix/endpoint/sync_code_reload_plug.ex:22: Phoenix.Endpoint.SyncCodeReloadPlug.do_call/4
    (bandit 1.5.7) lib/bandit/pipeline.ex:124: Bandit.Pipeline.call_plug!/2
    (bandit 1.5.7) lib/bandit/pipeline.ex:36: Bandit.Pipeline.run/4
    (bandit 1.5.7) lib/bandit/http1/handler.ex:12: Bandit.HTTP1.Handler.handle_data/3
    (bandit 1.5.7) lib/bandit/delegating_handler.ex:18: Bandit.DelegatingHandler.handle_data/3
    (bandit 1.5.7) /var/home/sezdocs/projects/rebuilt/platform/core/deps/thousand_island/lib/thousand_island/handler.ex:411: Bandit.DelegatingHandler.handle_continue/2
    (stdlib 6.0.1) gen_server.erl:2163: :gen_server.try_handle_continue/3
    (stdlib 6.0.1) gen_server.erl:2072: :gen_server.loop/7
    (stdlib 6.0.1) proc_lib.erl:329: :proc_lib.init_p_do_apply/3

And the following error as a response in graphql:

{
  "data": {
    "listValidProperties": null
  },
  "errors": [
    {
      "message": "Something went wrong. Unique error id: `96e7c8cc-071b-4623-a8a3-cad0b578f02f`",
      "path": [
        "listValidProperties"
      ],
      "locations": [
        {
          "line": 2,
          "column": 2
        }
      ]
    }
  ]
}

This used to work in Ash2 btw

Showing Posts 17 to 8

zachdaniel

zachdaniel

Creator of Ash

The add_calculations callback is where we determine the calculations, and then in add_field_level_auth function in the read action logic.

sezaru

sezaru OP

Would you mind giving me some hints which files/functions the field policies code path is run? I can take a look into it when I have some free time in my hands.

zachdaniel

zachdaniel

Creator of Ash

Yeah, I’d imagine this area is rife for optimizations :slight_smile:

Ultimately, I think having fewer field policies is better than one policy per field from a conceptual standpoint, and we should come up with a way for you to write either a deny-list or an allow-list.

I’d be willing to bet that we could eliminate most if not all of that additional processing time via optimizing those code paths. If you could open an issue on Ash showing this info that would be great. I won’t have the time to optimize this in the near future, unfortunately, but perhaps an adventurous soul can investigate.

sezaru

sezaru OP

I just added some more benchmark numbers:

Name                       ips        average  deviation         median         99th %
disabled               4885.16        0.20 ms    ±16.71%       0.197 ms        0.34 ms
catch_all_always       1227.80        0.81 ms    ±22.45%        0.75 ms        1.61 ms
catch_all_roles         964.08        1.04 ms    ±13.55%        1.00 ms        1.57 ms
always                   50.60       19.76 ms    ±19.98%       19.15 ms       30.24 ms
bypass_roles             37.81       26.45 ms    ±13.31%       26.03 ms       35.65 ms
roles                    36.92       27.09 ms    ±11.88%       26.44 ms       38.14 ms

Comparison: 
disabled               4885.16
catch_all_always       1227.80 - 3.98x slower +0.61 ms
catch_all_roles         964.08 - 5.07x slower +0.83 ms
always                   50.60 - 96.55x slower +19.56 ms
bypass_roles             37.81 - 129.19x slower +26.24 ms
roles                    36.92 - 132.33x slower +26.88 ms

Memory usage statistics:

Name                     average  deviation         median         99th %
disabled               0.0254 MB     ±0.01%      0.0254 MB      0.0254 MB
catch_all_always        0.142 MB     ±0.00%       0.142 MB       0.142 MB
catch_all_roles          0.29 MB     ±0.01%        0.29 MB        0.29 MB
always                   5.22 MB     ±0.06%        5.22 MB        5.23 MB
bypass_roles            15.10 MB     ±0.13%       15.10 MB       15.15 MB
roles                   15.31 MB     ±0.06%       15.31 MB       15.34 MB

Comparison: 
disabled               0.0254 MB
catch_all_always        0.142 MB - 5.62x memory usage +0.117 MB
catch_all_roles          0.29 MB - 11.45x memory usage +0.27 MB
always                   5.22 MB - 205.84x memory usage +5.20 MB
bypass_roles            15.10 MB - 595.18x memory usage +15.07 MB
roles                   15.31 MB - 603.57x memory usage +15.29 MB

Legend:
disabled: field_policies are fully disabled
catch_all_always: there is only one field_policy with a catch_all that always authorize
catch_all_roles: there is only one field_policy with a catch_all that checks for the role
always: have a field_policy per attribute that always authorize
bypass_roles: have a field_policy_bypass per attribute that will check for the role
roles: have a field_policy per attribute that will check for the role

It is interesting to see that even having just one field_policy already makes the query 4x slower than not having any check at all

sezaru

sezaru OP

So, @zachdaniel , I did some benchmark testing here and I think I got some interesting numbers.

The test consists of a resource that contains around 255 attributes. I used the ETS data-layer and added one record to it.

Then, I run a read action to fetch it applying my field policies and another run not applying it.

The benchmark code is as follows:

defmodule Benchmark do
  alias Core.Pacman.Markets.Record
  
  def run() do
    actor = %{roles: [:blibs]}

    Benchee.run(
      %{
        "run" => fn ->
          Record
          |> Ash.Query.for_read(:read, %{}, actor: actor)
          |> Ash.read!()
        end,
      },
      time: 100,
      memory_time: 2
    )
  end
end

For the field policies, I added one per attribute, all of them have the same check, I did two types of checks to see if the check itself would be a bottleneck.

Using a check that search for a atom in the :roles field of an agent (based on the HasRole check from ash rbac):

      field_policy :external_id do
        authorize_if {HasRole, role: [:support, :admin, {:agent, :organization_roles}]}
      end

Using a always authorize check:

      field_policy :external_id do
        authorize_if always()
      end

Here are the results:

Name               ips        average  deviation         median         99th %
disabled       4885.16        0.20 ms    ±16.71%       0.197 ms        0.34 ms
always           50.60       19.76 ms    ±19.98%       19.15 ms       30.24 ms
roles            36.92       27.09 ms    ±11.88%       26.44 ms       38.14 ms

Comparison: 
disabled       4885.16
always           50.60 - 96.55x slower +19.56 ms
roles            36.92 - 132.33x slower +26.88 ms

Memory usage statistics:

Name             average  deviation         median         99th %
disabled       0.0254 MB     ±0.01%      0.0254 MB      0.0254 MB
always           5.22 MB     ±0.06%        5.22 MB        5.23 MB
roles           15.31 MB     ±0.06%       15.31 MB       15.34 MB

Comparison: 
disabled       0.0254 MB
always           5.22 MB - 205.84x memory usage +5.20 MB
roles           15.31 MB - 603.57x memory usage +15.29 MB

Seems like there is a very big overhead of creating field policies per field

sezaru

sezaru OP

I’m gonna try using the ets datalayer since I think that one would be more consistent on timing.

zachdaniel

zachdaniel

Creator of Ash

Ah, yeah you’re right :slight_smile: Wasn’t thinking. I don’t think there is a good way to just apply field policies TBH. They are implemented as calculations.

sezaru

sezaru OP

Hmm, why would that not go to the database? Shouldn’t it fetch all these fields in the load call? At least that’s what I’m seeing here when I tried it.

Here is my code:

records = Record |> Ash.Query.for_read(:read, %{}) |> Ash.Query.select([]) |> Ash.Query.limit(100) |>  Ash.read!()

fields = [,,,]

Ash.load!(records, fields, actor: actor)
zachdaniel

zachdaniel

Creator of Ash

Sort of. You can fetch a record with select([]) and then Ash.load(thing, [:your, :big, :list, :of :attributes], actor: actor). That tests a bit more than just field policies, but shouldn’t go to the database or anything.

sezaru

sezaru OP

@zachdaniel is there some call that I can do to just run the field_policies for a resource action without running the full action (like we can use Ash.can? for normal policies)?

Basically I’m creating some benchmarks to test if there is any impact in performance, but it would be great if I can shrink the scope just to the field policies.

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New
ryanwinchester
apply_graft/2 doesn’t rewrite an add_many sub-workflow’s deps on an add step. Grafted jobs cancel with “upstream job was deleted” Version...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews