Просмотр исходного кода

fix: disabled action no longer cause error log and failure counter

prior to this fix, if an action is disabled, a warning log is emitted
and failure counter is incremented.
this fix introduced a new rule counter named actions.discarded
to record the number of discarded action matches due to action
being disabled or due to a race condition while the action is
being deleted
zmstone 1 год назад
Родитель
Сommit
172e6cdc33

+ 1 - 1
apps/emqx_bridge/src/emqx_bridge.app.src

@@ -1,7 +1,7 @@
 %% -*- mode: erlang -*-
 {application, emqx_bridge, [
     {description, "EMQX bridges"},
-    {vsn, "0.2.4"},
+    {vsn, "0.2.5"},
     {registered, [emqx_bridge_sup]},
     {mod, {emqx_bridge_app, []}},
     {applications, [

+ 2 - 1
apps/emqx_bridge/src/emqx_bridge.erl

@@ -256,7 +256,8 @@ send_message(BridgeType, BridgeName, ResId, Message, QueryOpts0) ->
             QueryOpts = maps:merge(query_opts(Config), QueryOpts0),
             emqx_resource:query(ResId, {send_message, Message}, QueryOpts);
         #{enable := false} ->
-            {error, bridge_stopped}
+            %% race
+            {error, bridge_disabled}
     end.
 
 query_opts(Config) ->

+ 7 - 5
apps/emqx_bridge/src/emqx_bridge_v2.erl

@@ -670,8 +670,9 @@ query(BridgeType, BridgeName, Message, QueryOpts0) ->
             Config = combine_connector_and_bridge_v2_config(BridgeType, BridgeName, Config0),
             do_query_with_enabled_config(BridgeType, BridgeName, Message, QueryOpts0, Config);
         #{enable := false} ->
-            {error, bridge_stopped};
-        _Error ->
+            {error, bridge_disabled};
+        {error, bridge_not_found} ->
+            %% race
             {error, bridge_not_found}
     end.
 
@@ -725,9 +726,10 @@ health_check(ConfRootKey, BridgeType, BridgeName) ->
                 ConnectorId, id_with_root_name(ConfRootKey, BridgeType, BridgeName, ConnectorName)
             );
         #{enable := false} ->
-            {error, bridge_stopped};
-        Error ->
-            Error
+            {error, bridge_disabled};
+        {error, bridge_not_found} ->
+            %% race
+            {error, bridge_not_found}
     end.
 
 -spec create_dry_run(bridge_v2_type(), Config :: map()) -> ok | {error, term()}.

+ 139 - 75
apps/emqx_bridge_http/test/emqx_bridge_http_v2_SUITE.erl

@@ -126,84 +126,148 @@ t_compose_connector_url_and_action_path(Config) ->
 %% Checks that we can successfully update a connector containing sensitive headers and
 %% they won't be clobbered by the update.
 t_update_with_sensitive_data(Config) ->
-    ?check_trace(
-        begin
-            ConnectorCfg0 = make_connector_config(Config),
-            AuthHeader = <<"Bearer some_token">>,
-            ConnectorCfg1 = emqx_utils_maps:deep_merge(
-                ConnectorCfg0,
-                #{
-                    <<"headers">> => #{
-                        <<"authorization">> => AuthHeader,
-                        <<"x-test-header">> => <<"from-connector">>
-                    }
-                }
-            ),
-            ActionCfg = make_action_config(Config, #{<<"x-test-header">> => <<"from-action">>}),
-            CreateConfig = [
-                {bridge_kind, action},
-                {action_type, ?BRIDGE_TYPE},
-                {action_name, ?BRIDGE_NAME},
-                {action_config, ActionCfg},
-                {connector_type, ?BRIDGE_TYPE},
-                {connector_name, ?CONNECTOR_NAME},
-                {connector_config, ConnectorCfg1}
-            ],
-            {ok, {{_, 201, _}, _, #{<<"headers">> := #{<<"authorization">> := Obfuscated}}}} =
-                emqx_bridge_v2_testlib:create_connector_api(CreateConfig),
-            {ok, _} =
-                emqx_bridge_v2_testlib:create_kind_api(CreateConfig),
-            BridgeId = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, ?BRIDGE_NAME),
-            {ok, _} = emqx_bridge_v2_testlib:create_rule_api(
-                #{
-                    sql => <<"select * from \"t/http\" ">>,
-                    actions => [BridgeId]
-                }
-            ),
-            emqx:publish(emqx_message:make(<<"t/http">>, <<"1">>)),
-            ?assertReceive(
-                {http,
-                    #{
-                        <<"authorization">> := AuthHeader,
-                        <<"x-test-header">> := <<"from-action">>
-                    },
-                    _}
-            ),
+    ConnectorCfg0 = make_connector_config(Config),
+    AuthHeader = <<"Bearer some_token">>,
+    ConnectorCfg1 = emqx_utils_maps:deep_merge(
+        ConnectorCfg0,
+        #{
+            <<"headers">> => #{
+                <<"authorization">> => AuthHeader,
+                <<"x-test-header">> => <<"from-connector">>
+            }
+        }
+    ),
+    ActionCfg = make_action_config(Config, #{<<"x-test-header">> => <<"from-action">>}),
+    CreateConfig = [
+        {bridge_kind, action},
+        {action_type, ?BRIDGE_TYPE},
+        {action_name, ?BRIDGE_NAME},
+        {action_config, ActionCfg},
+        {connector_type, ?BRIDGE_TYPE},
+        {connector_name, ?CONNECTOR_NAME},
+        {connector_config, ConnectorCfg1}
+    ],
+    {ok, {{_, 201, _}, _, #{<<"headers">> := #{<<"authorization">> := Obfuscated}}}} =
+        emqx_bridge_v2_testlib:create_connector_api(CreateConfig),
+    {ok, _} =
+        emqx_bridge_v2_testlib:create_kind_api(CreateConfig),
+    BridgeId = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, ?BRIDGE_NAME),
+    {ok, _} = emqx_bridge_v2_testlib:create_rule_api(
+        #{
+            sql => <<"select * from \"t/http\" ">>,
+            actions => [BridgeId]
+        }
+    ),
+    emqx:publish(emqx_message:make(<<"t/http">>, <<"1">>)),
+    ?assertReceive(
+        {http,
+            #{
+                <<"authorization">> := AuthHeader,
+                <<"x-test-header">> := <<"from-action">>
+            },
+            _}
+    ),
+
+    %% Now update the connector and see if the header stays deobfuscated.  We send the old
+    %% auth header as an obfuscated value to simulate the behavior of the frontend.
+    ConnectorCfg2 = emqx_utils_maps:deep_merge(
+        ConnectorCfg1,
+        #{
+            <<"headers">> => #{
+                <<"authorization">> => Obfuscated,
+                <<"x-test-header">> => <<"from-connector-new">>,
+                <<"x-test-header-2">> => <<"from-connector-new">>,
+                <<"other_header">> => <<"new">>
+            }
+        }
+    ),
+    {ok, _} = emqx_bridge_v2_testlib:update_connector_api(
+        ?CONNECTOR_NAME,
+        ?BRIDGE_TYPE,
+        ConnectorCfg2
+    ),
 
-            %% Now update the connector and see if the header stays deobfuscated.  We send the old
-            %% auth header as an obfuscated value to simulate the behavior of the frontend.
-            ConnectorCfg2 = emqx_utils_maps:deep_merge(
-                ConnectorCfg1,
-                #{
-                    <<"headers">> => #{
-                        <<"authorization">> => Obfuscated,
-                        <<"x-test-header">> => <<"from-connector-new">>,
-                        <<"x-test-header-2">> => <<"from-connector-new">>,
-                        <<"other_header">> => <<"new">>
-                    }
+    emqx:publish(emqx_message:make(<<"t/http">>, <<"2">>)),
+    %% Should not be obfuscated.
+    ?assertReceive(
+        {http,
+            #{
+                <<"authorization">> := AuthHeader,
+                <<"x-test-header">> := <<"from-action">>,
+                <<"x-test-header-2">> := <<"from-connector-new">>
+            },
+            _},
+        2_000
+    ),
+    ok.
+
+t_disable_action_counters(Config) ->
+    ConnectorCfg = make_connector_config(Config),
+    ActionCfg = make_action_config(Config),
+    CreateConfig = [
+        {bridge_kind, action},
+        {action_type, ?BRIDGE_TYPE},
+        {action_name, ?BRIDGE_NAME},
+        {action_config, ActionCfg},
+        {connector_type, ?BRIDGE_TYPE},
+        {connector_name, ?CONNECTOR_NAME},
+        {connector_config, ConnectorCfg}
+    ],
+    {ok, {{_, 201, _}, _, _}} =
+        emqx_bridge_v2_testlib:create_connector_api(CreateConfig),
+    {ok, _} =
+        emqx_bridge_v2_testlib:create_kind_api(CreateConfig),
+    BridgeId = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, ?BRIDGE_NAME),
+    {ok, Rule} = emqx_bridge_v2_testlib:create_rule_api(
+        #{
+            sql => <<"select * from \"t/http\" ">>,
+            actions => [BridgeId]
+        }
+    ),
+    {{_, 201, _}, _, #{<<"id">> := RuleId}} = Rule,
+    emqx:publish(emqx_message:make(<<"t/http">>, <<"1">>)),
+    ?assertReceive({http_server, received, _}, 2_000),
+
+    ?retry(
+        _Interval = 500,
+        _NAttempts = 20,
+        ?assertMatch(
+            #{
+                counters := #{
+                    'matched' := 1,
+                    'actions.failed' := 0,
+                    'actions.failed.unknown' := 0,
+                    'actions.success' := 1,
+                    'actions.total' := 1,
+                    'actions.discarded' := 0
                 }
-            ),
-            {ok, _} = emqx_bridge_v2_testlib:update_connector_api(
-                ?CONNECTOR_NAME,
-                ?BRIDGE_TYPE,
-                ConnectorCfg2
-            ),
+            },
+            emqx_metrics_worker:get_metrics(rule_metrics, RuleId)
+        )
+    ),
 
-            emqx:publish(emqx_message:make(<<"t/http">>, <<"2">>)),
-            %% Should not be obfuscated.
-            ?assertReceive(
-                {http,
-                    #{
-                        <<"authorization">> := AuthHeader,
-                        <<"x-test-header">> := <<"from-action">>,
-                        <<"x-test-header-2">> := <<"from-connector-new">>
-                    },
-                    _},
-                2_000
-            ),
-            ok
-        end,
-        []
+    %% disable the action
+    {ok, {{_, 200, _}, _, _}} =
+        emqx_bridge_v2_testlib:update_bridge_api(CreateConfig, #{<<"enable">> => false}),
+
+    %% this will trigger a discard
+    emqx:publish(emqx_message:make(<<"t/http">>, <<"2">>)),
+    ?retry(
+        _Interval = 500,
+        _NAttempts = 20,
+        ?assertMatch(
+            #{
+                counters := #{
+                    'matched' := 2,
+                    'actions.failed' := 0,
+                    'actions.failed.unknown' := 0,
+                    'actions.success' := 1,
+                    'actions.total' := 2,
+                    'actions.discarded' := 1
+                }
+            },
+            emqx_metrics_worker:get_metrics(rule_metrics, RuleId)
+        )
     ),
 
     ok.

+ 3 - 1
apps/emqx_bridge_kafka/test/emqx_bridge_kafka_impl_producer_SUITE.erl

@@ -358,6 +358,7 @@ kafka_bridge_rest_api_helper(Config) ->
         ?assertEqual(1, emqx_resource_metrics:success_get(BridgeV2Id)),
         ?assertEqual(1, emqx_metrics_worker:get(rule_metrics, RuleId, 'actions.success')),
         ?assertEqual(0, emqx_metrics_worker:get(rule_metrics, RuleId, 'actions.failed')),
+        ?assertEqual(0, emqx_metrics_worker:get(rule_metrics, RuleId, 'actions.discarded')),
         ?assertEqual(0, emqx_resource_metrics:dropped_get(BridgeV2Id)),
         ?assertEqual(0, emqx_resource_metrics:failed_get(BridgeV2Id)),
         ?assertEqual(0, emqx_resource_metrics:inflight_get(BridgeV2Id)),
@@ -377,7 +378,8 @@ kafka_bridge_rest_api_helper(Config) ->
         timer:sleep(100),
         ?assertEqual(0, emqx_resource_metrics:success_get(BridgeV2Id)),
         ?assertEqual(1, emqx_metrics_worker:get(rule_metrics, RuleId, 'actions.success')),
-        ?assertEqual(1, emqx_metrics_worker:get(rule_metrics, RuleId, 'actions.failed')),
+        ?assertEqual(0, emqx_metrics_worker:get(rule_metrics, RuleId, 'actions.failed')),
+        ?assertEqual(1, emqx_metrics_worker:get(rule_metrics, RuleId, 'actions.discarded')),
         {ok, 204, _} = http_put(BridgesPartsOpDisable, #{}),
         {ok, 204, _} = http_put(BridgesPartsOpEnable, #{}),
         ?assertEqual(0, emqx_resource_metrics:success_get(BridgeV2Id)),

+ 1 - 1
apps/emqx_prometheus/src/emqx_prometheus.app.src

@@ -2,7 +2,7 @@
 {application, emqx_prometheus, [
     {description, "Prometheus for EMQX"},
     % strict semver, bump manually!
-    {vsn, "5.2.4"},
+    {vsn, "5.2.5"},
     {modules, []},
     {registered, [emqx_prometheus_sup]},
     {applications, [kernel, stdlib, prometheus, emqx, emqx_auth, emqx_resource, emqx_management]},

+ 5 - 2
apps/emqx_prometheus/src/emqx_prometheus_data_integration.erl

@@ -225,6 +225,7 @@ collect_di(K = emqx_rule_actions_success, Data) -> counter_metrics(?MG(K, Data))
 collect_di(K = emqx_rule_actions_failed, Data) -> counter_metrics(?MG(K, Data));
 collect_di(K = emqx_rule_actions_failed_out_of_service, Data) -> counter_metrics(?MG(K, Data));
 collect_di(K = emqx_rule_actions_failed_unknown, Data) -> counter_metrics(?MG(K, Data));
+collect_di(K = emqx_rule_actions_discarded, Data) -> counter_metrics(?MG(K, Data));
 %%====================
 %% Action Metric
 collect_di(K = emqx_action_enable, Data) -> gauge_metrics(?MG(K, Data));
@@ -379,7 +380,8 @@ rule_metric_meta() ->
         {emqx_rule_actions_success, counter},
         {emqx_rule_actions_failed, counter},
         {emqx_rule_actions_failed_out_of_service, counter},
-        {emqx_rule_actions_failed_unknown, counter}
+        {emqx_rule_actions_failed_unknown, counter},
+        {emqx_rule_actions_discarded, counter}
     ].
 
 rule_metric(names) ->
@@ -420,7 +422,8 @@ get_metric(#{id := Id, enable := Bool} = _Rule) ->
         emqx_rule_actions_success => ?MG('actions.success', Counters),
         emqx_rule_actions_failed => ?MG('actions.failed', Counters),
         emqx_rule_actions_failed_out_of_service => ?MG('actions.failed.out_of_service', Counters),
-        emqx_rule_actions_failed_unknown => ?MG('actions.failed.unknown', Counters)
+        emqx_rule_actions_failed_unknown => ?MG('actions.failed.unknown', Counters),
+        emqx_rule_actions_discarded => ?MG('actions.discarded', Counters)
     }.
 
 %%====================

+ 4 - 0
apps/emqx_rule_engine/src/emqx_rule_api_schema.erl

@@ -165,6 +165,10 @@ fields("metrics") ->
         {"actions.failed.unknown",
             sc(non_neg_integer(), #{
                 desc => ?DESC("metrics_actions_failed_unknown")
+            })},
+        {"actions.discarded",
+            sc(non_neg_integer(), #{
+                desc => ?DESC("metrics_actions_discarded")
             })}
     ];
 fields("node_metrics") ->

+ 1 - 1
apps/emqx_rule_engine/src/emqx_rule_engine.app.src

@@ -2,7 +2,7 @@
 {application, emqx_rule_engine, [
     {description, "EMQX Rule Engine"},
     % strict semver, bump manually!
-    {vsn, "5.2.0"},
+    {vsn, "5.2.1"},
     {modules, []},
     {registered, [emqx_rule_engine_sup, emqx_rule_engine]},
     {applications, [

+ 2 - 1
apps/emqx_rule_engine/src/emqx_rule_engine.erl

@@ -104,7 +104,8 @@
     'actions.success',
     'actions.failed',
     'actions.failed.out_of_service',
-    'actions.failed.unknown'
+    'actions.failed.unknown',
+    'actions.discarded'
 ]).
 
 -define(RATE_METRICS, ['matched']).

+ 53 - 73
apps/emqx_rule_engine/src/emqx_rule_engine_api.erl

@@ -62,7 +62,8 @@ end).
     end
 ).
 
--define(METRICS(
+%% Metrics map value
+-define(METRICS_VAL(
     MATCH,
     PASS,
     FAIL,
@@ -73,6 +74,7 @@ end).
     O_FAIL_OOS,
     O_FAIL_UNKNOWN,
     O_SUCC,
+    O_DISCARDED,
     RATE,
     RATE_MAX,
     RATE_5
@@ -88,13 +90,17 @@ end).
         'actions.failed.out_of_service' => O_FAIL_OOS,
         'actions.failed.unknown' => O_FAIL_UNKNOWN,
         'actions.success' => O_SUCC,
+        'actions.discarded' => O_DISCARDED,
         'matched.rate' => RATE,
         'matched.rate.max' => RATE_MAX,
         'matched.rate.last5m' => RATE_5
     }
 ).
 
--define(metrics(
+-define(METRICS_VAL_ZERO, ?METRICS_VAL(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)).
+
+%% Metrics map match pattern
+-define(METRICS_PAT(
     MATCH,
     PASS,
     FAIL,
@@ -105,6 +111,7 @@ end).
     O_FAIL_OOS,
     O_FAIL_UNKNOWN,
     O_SUCC,
+    O_DISCARDED,
     RATE,
     RATE_MAX,
     RATE_5
@@ -120,6 +127,7 @@ end).
         'actions.failed.out_of_service' := O_FAIL_OOS,
         'actions.failed.unknown' := O_FAIL_UNKNOWN,
         'actions.success' := O_SUCC,
+        'actions.discarded' := O_DISCARDED,
         'matched.rate' := RATE,
         'matched.rate.max' := RATE_MAX,
         'matched.rate.last5m' := RATE_5
@@ -633,7 +641,8 @@ format_metrics(Node, #{
             'actions.failed' := OFailed,
             'actions.failed.out_of_service' := OFailedOOS,
             'actions.failed.unknown' := OFailedUnknown,
-            'actions.success' := OFailedSucc
+            'actions.success' := OSucc,
+            'actions.discarded' := ODiscard
         },
     rate :=
         #{
@@ -642,7 +651,7 @@ format_metrics(Node, #{
         }
 }) ->
     #{
-        metrics => ?METRICS(
+        metrics => ?METRICS_VAL(
             Matched,
             Passed,
             Failed,
@@ -652,7 +661,8 @@ format_metrics(Node, #{
             OFailed,
             OFailedOOS,
             OFailedUnknown,
-            OFailedSucc,
+            OSucc,
+            ODiscard,
             Current,
             Max,
             Last5M
@@ -663,79 +673,49 @@ format_metrics(Node, _Metrics) ->
     %% Empty metrics: can happen when a node joins another and a bridge is not yet
     %% replicated to it, so the counters map is empty.
     #{
-        metrics => ?METRICS(
-            _Matched = 0,
-            _Passed = 0,
-            _Failed = 0,
-            _FailedEx = 0,
-            _FailedNoRes = 0,
-            _OTotal = 0,
-            _OFailed = 0,
-            _OFailedOOS = 0,
-            _OFailedUnknown = 0,
-            _OFailedSucc = 0,
-            _Current = 0,
-            _Max = 0,
-            _Last5M = 0
-        ),
+        metrics => ?METRICS_VAL_ZERO,
         node => Node
     }.
 
 aggregate_metrics(AllMetrics) ->
-    InitMetrics = ?METRICS(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
-    lists:foldl(
-        fun(
-            #{
-                metrics := ?metrics(
-                    Match1,
-                    Passed1,
-                    Failed1,
-                    FailedEx1,
-                    FailedNoRes1,
-                    OTotal1,
-                    OFailed1,
-                    OFailedOOS1,
-                    OFailedUnknown1,
-                    OFailedSucc1,
-                    Rate1,
-                    RateMax1,
-                    Rate5m1
-                )
-            },
-            ?metrics(
-                Match0,
-                Passed0,
-                Failed0,
-                FailedEx0,
-                FailedNoRes0,
-                OTotal0,
-                OFailed0,
-                OFailedOOS0,
-                OFailedUnknown0,
-                OFailedSucc0,
-                Rate0,
-                RateMax0,
-                Rate5m0
-            )
-        ) ->
-            ?METRICS(
-                Match1 + Match0,
-                Passed1 + Passed0,
-                Failed1 + Failed0,
-                FailedEx1 + FailedEx0,
-                FailedNoRes1 + FailedNoRes0,
-                OTotal1 + OTotal0,
-                OFailed1 + OFailed0,
-                OFailedOOS1 + OFailedOOS0,
-                OFailedUnknown1 + OFailedUnknown0,
-                OFailedSucc1 + OFailedSucc0,
-                Rate1 + Rate0,
-                RateMax1 + RateMax0,
-                Rate5m1 + Rate5m0
-            )
+    InitMetrics = ?METRICS_VAL_ZERO,
+    lists:foldl(fun do_aggregate_metrics/2, InitMetrics, AllMetrics).
+
+do_aggregate_metrics(#{metrics := Mt1}, Mt0) when map_size(Mt1) =:= map_size(Mt0) ->
+    ?METRICS_PAT(A1, B1, C1, D1, E1, F1, G1, H1, I1, J1, K1, L1, M1, N1) = Mt1,
+    ?METRICS_PAT(A0, B0, C0, D0, E0, F0, G0, H0, I0, J0, K0, L0, M0, N0) = Mt0,
+    ?METRICS_VAL(
+        A0 + A1,
+        B0 + B1,
+        C0 + C1,
+        D0 + D1,
+        E0 + E1,
+        F0 + F1,
+        G0 + G1,
+        H0 + H1,
+        I0 + I1,
+        J0 + J1,
+        K0 + K1,
+        L0 + L1,
+        M0 + M1,
+        N0 + N1
+    );
+do_aggregate_metrics(#{metrics := M1}, M0) ->
+    %% this happens during rolling upgrade
+    %% fallback to per-map-key iteration
+    maps:fold(
+        fun(Name, V1, Acc) ->
+            case maps:get(Name, Acc, false) of
+                false ->
+                    %% this is an unknown metric name for this node
+                    %% discard
+                    Acc;
+                V0 ->
+                    Acc#{Name => V0 + V1}
+            end
         end,
-        InitMetrics,
-        AllMetrics
+        M0,
+        M1
     ).
 
 add_metadata(Params) ->

+ 8 - 6
apps/emqx_rule_engine/src/emqx_rule_runtime.erl

@@ -402,8 +402,7 @@ handle_action_list(RuleId, Actions, Selected, Envs) ->
 handle_action(RuleId, ActId, Selected, Envs) ->
     ok = emqx_metrics_worker:inc(rule_metrics, RuleId, 'actions.total'),
     try
-        Result = do_handle_action(RuleId, ActId, Selected, Envs),
-        Result
+        do_handle_action(RuleId, ActId, Selected, Envs)
     catch
         throw:out_of_service ->
             ok = emqx_metrics_worker:inc(rule_metrics, RuleId, 'actions.failed'),
@@ -411,6 +410,9 @@ handle_action(RuleId, ActId, Selected, Envs) ->
                 rule_metrics, RuleId, 'actions.failed.out_of_service'
             ),
             trace_action(ActId, "out_of_service", #{}, warning);
+        throw:{discard, Reason} ->
+            ok = emqx_metrics_worker:inc(rule_metrics, RuleId, 'actions.discarded'),
+            trace_action(ActId, "discarded", #{cause => Reason}, debug);
         error:?EMQX_TRACE_STOP_ACTION_MATCH = Reason ->
             ?EMQX_TRACE_STOP_ACTION(Explanation) = Reason,
             trace_action(
@@ -445,8 +447,8 @@ do_handle_action(RuleId, {bridge, BridgeType, BridgeName, ResId} = Action, Selec
             reply_to => ReplyTo, trace_ctx => TraceCtx
         })
     of
-        {error, Reason} when Reason == bridge_not_found; Reason == bridge_stopped ->
-            throw(out_of_service);
+        {error, Reason} when Reason == bridge_not_found; Reason == bridge_disabled ->
+            throw({discard, Reason});
         ?RESOURCE_ERROR_M(R, _) when ?IS_RES_DOWN(R) ->
             throw(out_of_service);
         Result ->
@@ -469,8 +471,8 @@ do_handle_action(
             #{reply_to => ReplyTo, trace_ctx => TraceCtx}
         )
     of
-        {error, Reason} when Reason == bridge_not_found; Reason == bridge_stopped ->
-            throw(out_of_service);
+        {error, Reason} when Reason == bridge_not_found; Reason == bridge_disabled ->
+            throw({discard, Reason});
         ?RESOURCE_ERROR_M(R, _) when ?IS_RES_DOWN(R) ->
             throw(out_of_service);
         Result ->

+ 4 - 2
apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl

@@ -3755,13 +3755,15 @@ t_get_rule_ids_by_action_reference_ingress_bridge(_Config) ->
     matched := 1,
     'actions.total' := 1,
     'actions.failed' := 0,
-    'actions.success' := 1
+    'actions.success' := 1,
+    'actions.discarded' := 0
 }).
 -define(FAIL_METRICS, #{
     matched := 1,
     'actions.total' := 1,
     'actions.failed' := 1,
-    'actions.success' := 0
+    'actions.success' := 0,
+    'actions.discarded' := 0
 }).
 
 t_rule_metrics_sync(_Config) ->

+ 7 - 0
changes/ce/feat-13773.en.md

@@ -0,0 +1,7 @@
+Disabled rule actions now no trigger emits `out_of_service` warning.
+
+Previously, if an action is disabled, there would be a warning log with `msg: out_of_service`,
+and the `actions.failed` counter is incremented for the rule.
+
+After this enhancement, disabled action will result in a `debug` level log with `msg: discarded`,
+and the newly introduced counter `actions.discarded` will be incremented.

+ 6 - 0
rel/i18n/emqx_rule_api_schema.hocon

@@ -24,6 +24,12 @@ metrics_actions_failed_unknown.desc:
 metrics_actions_failed_unknown.label:
 """Unknown Failures"""
 
+metrics_actions_discarded.desc:
+"""The number of discarded actions due to action is disabled or race condition while action is being deleted."""
+
+metrics_actions_discarded.label:
+"""Discarded"""
+
 event_server.desc:
 """The IP address (or hostname) and port of the MQTT broker, in IP:Port format"""