BadMapError{term: nil}, geo - %Schema.Geo

Hi friends, I’m totally newbie, but since I see this error I can’t figure out what if causing this error.

This error triggered on a recurrent cron task which checks db for flag then script gets token to fetch data from google maps API.

Some tokens are going well, and script successfully fetched and stored data in DB, some with such error fails and stops script. Error stays the same until i delete unprocessible data.

The only tokens are changing and used to fetch data from google maps api.

To stop this error I have to go do db and delete unwanted data which can’t be processed due unknown (for me) issue.

Where to go to check the initial reason why this failures appear?

2022-10-20 23:30:01.455 [error] Handle geo point error - %BadMapError{term: nil}, geo - %Schema.Geo{__meta__: #Ecto.Schema.Metadata<:loaded, "geos">, ancestry: nil, ancestry_depth: 0, awards: #Ecto.Association.NotLoaded<association :awards is not loaded>, companies: #Ecto.Association.NotLoaded<association :companies is not loaded>, country_info: #Ecto.Association.NotLoaded<association :country_info is not loaded>, country_info_id: nil, created_at: ~N[2022-10-20 21:15:44.996481], full_name: nil, groups: #Ecto.Association.NotLoaded<association :groups is not loaded>, id: "18739c62-a8f5-4528-ad27-5610d30be8e4", kind: "latlng", latitude: 36.627206160762825, legacy_id: 3058, longitude: 31.76591573513031, name: nil, state: "draft", translations: nil, units: #Ecto.Association.NotLoaded<association :units is not loaded>, updated_at: ~N[2022-10-20 21:15:44.996481]}

the db record of an affecting entry is this

and the kind is a flag and latitude+longitude (geopoint) is the token to fetch data

After the script is rightly finished, it fills changes the flag to type localities fetched from g.api data, and is translations and puts in a translations jsonb with fetched translations for different langs.

Firstly script finds the country associated with geopoint, then checks if same data collected earlier and deletes this entry if duplicate found, then updates the id in related tables to that one which is already in db.

If no dups found it fetches data from google map API and builds multiple records, yet also checks for dups before creation, then fills the ancestry so keep the logic of google API in our proxy geo db where we store different languages needed to show the data w/o need to fetch every time google api.

part of schema in fetching script


  1 defmodule Mix.Tasks.Geos.CompleteAncestry do

  2   use Mix.Task

  3   import Ecto.Query, only: [from: 2]
  4   require Logger
  5   require IEx
  6
  7   @api_key "APITOKEN"
  8   @domain "https://maps.googleapis.com/maps/api/geocode/json"
  9   @localities [
 10     %{external_name: "country", name: "country"},
 11     %{external_name: "administrative_area_level_1", name: "division"},
 12     %{external_name: "administrative_area_level_2", name: "region"},
 13     %{external_name: "administrative_area_level_3", name: "subregion"},
 14     %{external_name: "administrative_area_level_4", name: "settlement"},
 15     %{external_name: "administrative_area_level_5", name: "village"},
 16     %{external_name: "locality", name: "city"},
 17     %{external_name: "sublocality_level_1", name: "district"},
 18     %{external_name: "sublocality_level_2", name: "locality"},
 19     %{external_name: "sublocality_level_3", name: "commune"},
 20     %{external_name: "sublocality_level_4", name: "commune1"},
 21     %{external_name: "sublocality_level_5", name: "commune2"},
 22     %{external_name: "establishment", name: "establishment"},
 23     %{external_name: "street_address", name: "street_address"},
 24     %{external_name: "neighborhood", name: "neighborhood"}
 25   ]
 26
 27   def run(args) do
 28     Application.ensure_all_started(:platform)
 29     time = DateTime.from_unix!(DateTime.to_unix(Timex.now()) - 5 * 60)
 30     from_time = DateTime.from_unix!(DateTime.to_unix(Timex.now()) - 15 * 60)
 31     Logger.info("Start complete ancestry time last - #{time}")
 32 # https://developers.google.com/maps/faq#languagesupport
 33 # https://developers.google.com/maps/documentation/geocoding/requests-geocoding#language-param
 34 # we may hit usage limits on number multiplied by locales > 5 https://developers.google.com/maps/faq?hl=en#usage-limits
 35 # 50 QPS is a limit, at max we fetch for 14th languages what seems to be 14 QPS
 36     Enum.each(get_points(args, time, from_time), fn record ->
 37       try do
 38         en = request_by_point(record, "en")
 39         de = request_by_point(record, "de")

[cutted]

 52         handle_localities(%{en: en, de: de, sv: sv, fi: fi, da: da, nl: nl, fr: fr, it: it, es: es, pt: pt, ru: ru, bg: bg, el: el, tr: tr}, record)
 53       rescue
 54         e ->
 55           Logger.error("Handle geo point error - #{inspect(e)}, geo - #{inspect(record)}")
 56       end
 57     end)
 58   end
 59
 60   def get_points(args, time, from_time) do
 61     cond do
 62       is_binary(args) && args == "old" -> Schema.Geo.get_old_by_latlng(time, from_time)
 63       true -> Schema.Geo.get_last_by_latlng(time)
 64     end
 65   end

[cutted]


123   def update_last_geo(record, point, country_info_id) do

124     cond do
125       point && point.geo && !point.exist ->
126         # if point geo not exist in database, update geo and delete last point
127         record
128         |> Schema.Geo.changeset(%{
129           kind: point.geo.kind,
130           state: point.geo.state,
131           ancestry_depth: point.geo.ancestry_depth,
132           ancestry: point.geo.ancestry,
133           name: point.geo.name,
134           full_name: point.geo.full_name,
135           country_info_id: country_info_id,
136           translations: point.geo.translations
137         })
138         |> Platform.Repo.update!()
139
140         update_geom(record)
141
142         Logger.info("Deleted temporary geo - #{inspect(point.geo)}")
143         point.geo |> Platform.Repo.delete()
144
145       point && point.geo && point.exist ->
146         #  if point geo exist in database, update all points in Units, Groups, CompanyOffices
147         update_relations(record.id, point.geo)
148
149         Logger.info("Deleted exist geo - #{inspect(record)}")
150         record |> Platform.Repo.delete()
151
152       true ->
153         Logger.info("Bad data for geo #{inspect(record)}")
154     end
155   end

[cutted]

212    cond do
213       geo ->
214         {geo, true}
215
216       true ->
217         country_info = List.first(ancestries).country_info
218         geo =
219           %Schema.Geo{}
220           |> Schema.Geo.changeset(%{
221             kind: kind,
222             state: state,
223             latitude: location["lat"],
224             longitude: location["lng"],
225             ancestry_depth: length(ancestries),
226             ancestry: Enum.map_join(ancestries, "/", & &1.id),
227             name: name,
228             full_name: full_name,
229             country_info_id: (if country_info, do: country_info.id, else: nil),
230             translations: %{
231               en: %{name: name, fullname: full_name},
232               de: %{
233                 name: build_name(result.de),
234                 fullname: build_full_name(ancestries, build_name(result.de), :de)
283                 },
[cutted]
284             }
285           })
286           |> Platform.Repo.insert!()
287
288         update_geom(geo)
289         {geo, false}
290     end
291   end

[cutted]

The error is happening in here, someplace, based on the message - the displayed BadMapError indicates that something passed nil where a Map was expected.

Consider adding additional output to that rescue to see the stacktrace:

rescue
  e ->
    Logger.error("etc etc etc etc\n#{Exception.format(:error, e, __STACKTRACE__)}")

Going to check.

I got this, but didn’t get it
When I change flagged record latitude/longitude to another one, same passes well.

2022-10-21 00:40:01.431 [error] My BadMapError
** (BadMapError) expected a map, got: nil
    (elixir) lib/map.ex:437: Map.get(nil, :name, nil)
    (platform) lib/platform/core/helpers/base.ex:21: Helpers.Base.get_in/2
    (elixir) lib/enum.ex:1314: Enum."-map/2-lists^map/1-0-"/2
    (platform) lib/mix/tasks/geos/complete_ancestry.ex:299: Mix.Tasks.Geos.CompleteAncestry.build_full_name/3
    (platform) lib/mix/tasks/geos/complete_ancestry.ex:239: Mix.Tasks.Geos.CompleteAncestry.find_or_create_geos/3
    (platform) lib/mix/tasks/geos/complete_ancestry.ex:98: anonymous fn/3 in Mix.Tasks.Geos.CompleteAncestry.handle_localities/2
    (elixir) lib/enum.ex:1925: Enum."-reduce/3-lists^foldl/2-0-"/3
    (platform) lib/mix/tasks/geos/complete_ancestry.ex:80: Mix.Tasks.Geos.CompleteAncestry.handle_localities/2

as i see in 21’st line

    (platform) lib/platform/core/helpers/base.ex:21: Helpers.Base.get_in/2

there is

  1 defmodule Helpers.Base do
  2   @moduledoc false
  3   def to_int(value) do
  4     case :string.to_integer(value) do
  5       {:error, _} -> value
  6       {int, _} -> int
  7     end
  8   end
  9
 10   @doc """
 11   Convert any type to string
 12   """
 13   def to_str(value) when is_atom(value), do: Atom.to_string(value)
 14   def to_str(value), do: inspect(value)
 15
 16   def to_negative(value), do: -1 * value
 17
 18   def get_in(object, list) do
 19     [source_column | path] = list
 20
 21     new_value = Map.get(object, source_column) || Map.get(object, to_str(source_column))
 22
 23     cond do
 24       length(path) == 0 -> new_value
 25       true -> __MODULE__.get_in(new_value, path)
 26     end
 27   end
 28 end
~
~

Seems like object is nil then.

This trace (along with the exception) tells you that build_full_name is calling get_in in a way that ultimately results in trying to do Map.get(nil, :name).

There are a lot of different ways to resolve this, depending on code that’s not visible:

  • get_in could have additional code added to guard against nil inputs. This could be as simple as an extra head def get_in(nil, _), do: nil
  • build_full_name could have additional code added to avoid calling get_in with values that cause a crash. Hard to say without any code from that function.
  • find_or_create_geos could have additional code added to avoid calling build_full_name with values that cause a crash. Again, hard to say without code.
  • either build_full_name or find_or_create_geos - or even some function farther up the stack - could have a bug that accidentally produces unexpected nils. For instance, a call to get_in(something, [:dr, :name]) where :dr is a typo for :de