Coverage Report

Created: 2026-09-04 06:54

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/gpowerd/state_updater.h
Line
Count
Source
1
#ifndef PRODUCTION_BORG_MGMT_NODE_PROXY_SAFEPOWER_SAFEPOWER_AGENT_STATE_UPDATER_H_
2
#define PRODUCTION_BORG_MGMT_NODE_PROXY_SAFEPOWER_SAFEPOWER_AGENT_STATE_UPDATER_H_
3
4
#include <algorithm>
5
#include <array>
6
#include <cstddef>
7
#include <list>
8
#include <memory>
9
#include <string>
10
#include <utility>
11
#include <vector>
12
13
#include "callback_manager.h"
14
#include "state_merge.h"
15
#include "absl/base/nullability.h"
16
#include "absl/base/thread_annotations.h"
17
#include "absl/functional/any_invocable.h"
18
#include "absl/functional/function_ref.h"
19
#include "absl/log/log.h"
20
#include "absl/synchronization/mutex.h"
21
22
namespace safepower_agent {
23
template <typename StateProto>
24
class StateUpdater {
25
 public:
26
  class Listener;
27
  using ListenerIterator = typename std::list<Listener* >::iterator;
28
29
  class Listener {
30
    struct StateUpdaterRef {
31
      std::weak_ptr<StateUpdater> ptr;
32
      StateUpdater* raw_ptr;  // For compares in destructor.
33
      ListenerIterator itr;
34
    };
35
36
   public:
37
    explicit Listener(bool is_passive_listener = false)
38
0
        : is_passive_listener_(is_passive_listener) {}
39
40
0
    virtual ~Listener() {
41
0
      if (!state_updater_.empty()) {
42
0
        LOG(DFATAL) << "Listener destroyed without calling StopListening";
43
0
      }
44
0
      StopListening();
45
0
    }
46
47
    // Derived classes must explicitly call this at the start of their
48
    // destructor if they access any member variables or resources during
49
    // callbacks, since those resources will be destroyed before the
50
    // base ~Listener() runs.
51
0
    void StopListening() {
52
0
      for (const StateUpdaterRef& ref : state_updater_) {
53
0
        std::shared_ptr<StateUpdater> state_updater = ref.ptr.lock();
54
0
        if (state_updater != nullptr) {
55
0
          state_updater->RemoveListener(ref.itr);
56
0
        }
57
0
      }
58
0
      state_updater_.clear();
59
0
    }
60
61
    template <typename... Args>
62
0
    void Listen(const Args&... args) {
63
0
      static_assert(sizeof...(args) > 0);
64
0
      if (!state_updater_.empty()) {
65
0
        LOG(DFATAL) << "Listener::Listen called multiple times";
66
0
        return;
67
0
      }
68
0
      state_updater_.reserve(sizeof...(args));
69
0
      std::array done = {Insert(args)...};
70
0
      for (size_t i = 0; i < done.size(); ++i) {
71
0
        if (done[i]) {
72
0
          MaybeDone(*state_updater_[i].raw_ptr);
73
0
        }
74
0
      }
75
0
    }
76
77
0
    bool IsPassiveListener() const { return is_passive_listener_; }
78
    virtual void UpdateState([[maybe_unused]] const StateProto& previous_state,
79
0
                             [[maybe_unused]] const StateProto& update) {
80
0
      LOG(DFATAL) << "UpdateState called on base Listener";
81
0
    }
82
83
0
    void MaybeDone(const StateUpdater& updater) {
84
0
      for (StateUpdaterRef& ref : state_updater_) {
85
0
        if (ref.raw_ptr == &updater) {
86
0
          ref.raw_ptr = nullptr;
87
0
          ++done_count_;
88
0
          if (done_count_ >= state_updater_.size()) {
89
0
            Done();
90
0
          }
91
0
          return;
92
0
        }
93
0
      }
94
0
      LOG(DFATAL) << "Listener not listening to this updater";
95
0
    }
96
97
   protected:
98
0
    virtual void Done() { LOG(DFATAL) << "Done called on base Listener"; }
99
100
   private:
101
0
    bool Insert(const  std::shared_ptr<StateUpdater>& updater) {
102
0
      for (const StateUpdaterRef& ref : state_updater_) {
103
0
        if (ref.ptr.lock() == updater) {
104
0
          LOG(DFATAL) << "Listener already listening to this updater";
105
0
          return true;
106
0
        }
107
0
      }
108
0
      bool final = updater->AddListener(
109
0
          *this, [&](ListenerIterator itr) {
110
0
            state_updater_.push_back({updater, updater.get(), itr});
111
0
          });
112
0
      return final;
113
0
    }
114
115
    std::vector<StateUpdaterRef> state_updater_;
116
    size_t done_count_ = 0;
117
    bool is_passive_listener_ = false;
118
  };
119
  friend class Listener;
120
121
  explicit StateUpdater(StateProto initial_state = {}, bool final_state = false)
122
      : state_(std::move(initial_state)), final_(final_state) {}
123
124
  ~StateUpdater() {
125
    if (!listeners_mutex_.WriterTryLock()) {
126
      LOG(DFATAL) << "StateUpdater destroyed while mutex was held";
127
      return;
128
    }
129
    for (Listener* listener : listeners_) {
130
      listener->MaybeDone(*this);
131
    }
132
    listeners_.clear();
133
    listeners_mutex_.WriterUnlock();
134
  }
135
136
  // Returns a copy of the state. If you want to access the state without
137
  // copying (e.g. for performance reasons with large states), prefer using
138
  // WithState.
139
  StateProto state() const {
140
    absl::MutexLock lock(&state_mutex_);
141
    return state_;
142
  }
143
144
  template <typename Func>
145
  auto WithState(Func&& f) const ABSL_LOCKS_EXCLUDED(state_mutex_) {
146
    absl::MutexLock lock(&state_mutex_);
147
    return std::forward<Func>(f)(state_);
148
  }
149
150
  std::string DebugString() const ABSL_LOCKS_EXCLUDED(state_mutex_) {
151
    return WithState(
152
        [](const StateProto& state) { return state.DebugString(); });
153
  }
154
155
  void UpdateState(const StateProto& update, bool final_state = false)
156
0
      ABSL_LOCKS_EXCLUDED(state_mutex_, listeners_mutex_) {
157
0
    absl::MutexLock lock_state(&state_mutex_);
158
0
    final_ = final_state;
159
0
    absl::MutexLock lock_listeners(&listeners_mutex_);
160
0
    for (auto listener : listeners_) {
161
0
      listener->UpdateState(state_, update);
162
0
      if (final_state) {
163
0
        listener->MaybeDone(*this);
164
0
      }
165
0
    }
166
0
    ApplyStateUpdate(state_, update);
167
0
  }
168
169
  // Runs a callback when the updater has no more listeners. The callback will
170
  // be called immediately if the updater is already idle. The move-only
171
  // returned handle can be destroyed to cancel the callback.
172
  // Idle callbacks are run in LIFO order, while active callbacks are run in
173
  // FIFO order. This reduces the likelihood of deadlocks: For example, if a
174
  // resource is acquired when active and released when idle, another pair of
175
  // callbacks that are added later will always see that resource acquired.
176
  CallbackManager::Handle OnIdle(absl::AnyInvocable<void() &&> callback)
177
      ABSL_LOCKS_EXCLUDED(listeners_mutex_) {
178
    if (IsIdle()) {
179
      std::move(callback)();
180
      return {};
181
    }
182
    return activity_callbacks_.RunFirst(std::move(callback));
183
  }
184
185
  // Runs a callback when the updater has at least one listener. The callback
186
  // will be called immediately if the updater already has listeners. See OnIdle
187
  // for more details.
188
  CallbackManager::Handle OnActive(absl::AnyInvocable<void() &&> callback)
189
      ABSL_LOCKS_EXCLUDED(listeners_mutex_) {
190
    if (!IsIdle()) {
191
      std::move(callback)();
192
      return {};
193
    }
194
    return activity_callbacks_.RunLast(std::move(callback));
195
  }
196
197
 private:
198
0
  bool IsIdleInternal() const ABSL_EXCLUSIVE_LOCKS_REQUIRED(listeners_mutex_) {
199
0
    for (const auto* listener : listeners_) {
200
0
      if (!listener->IsPassiveListener()) {
201
0
        return false;
202
0
      }
203
0
    }
204
0
    return true;
205
0
  }
206
207
  bool IsIdle() const ABSL_LOCKS_EXCLUDED(listeners_mutex_) {
208
    absl::MutexLock lock(&listeners_mutex_);
209
    return IsIdleInternal();
210
  }
211
212
  bool AddListener(Listener& listener,
213
                   absl::FunctionRef<void(ListenerIterator)> register_func)
214
0
      ABSL_LOCKS_EXCLUDED(state_mutex_, listeners_mutex_) {
215
0
    bool run_callbacks;
216
0
    bool result;
217
0
    {
218
0
      absl::MutexLock lock_state(&state_mutex_);
219
0
      listener.UpdateState({}, state_);
220
221
0
      absl::MutexLock lock_listeners(&listeners_mutex_);
222
      // IsIdleInternal() must be checked before push_front modifies the list
223
      // to correctly evaluate the pre-transition idle state.
224
0
      run_callbacks = IsIdleInternal() && !listener.IsPassiveListener();
225
      // push_front must happen before register_func so the iterator is valid.
226
0
      listeners_.push_front(&listener);
227
0
      register_func(listeners_.begin());
228
0
      result = final_;
229
0
    }
230
0
    if (run_callbacks) {
231
0
      activity_callbacks_.RunCallbacks();
232
0
    }
233
0
    return result;
234
0
  }
235
236
  void RemoveListener(ListenerIterator handle)
237
0
      ABSL_LOCKS_EXCLUDED(listeners_mutex_) {
238
0
    bool becomes_idle = false;
239
0
    {
240
0
      absl::MutexLock lock(&listeners_mutex_);
241
0
      if (handle == listeners_.end()) return;
242
0
      bool was_idle = IsIdleInternal();
243
0
      listeners_.erase(handle);
244
0
      becomes_idle = !was_idle && IsIdleInternal();
245
0
    }
246
0
    if (becomes_idle) {
247
0
      activity_callbacks_.RunCallbacks();
248
0
    }
249
0
  }
250
251
  mutable absl::Mutex state_mutex_ ABSL_ACQUIRED_BEFORE(listeners_mutex_);
252
  mutable absl::Mutex listeners_mutex_;
253
  // This is a std::list so that we have stable iterators
254
  std::list<Listener* > listeners_
255
      ABSL_GUARDED_BY(listeners_mutex_);
256
  StateProto state_ ABSL_GUARDED_BY(state_mutex_);
257
  bool final_ ABSL_GUARDED_BY(state_mutex_);
258
  // Callbacks that are run when the updater becomes idle (no listeners) or
259
  // active (at least one listener).
260
  CallbackManager activity_callbacks_;
261
};
262
263
}  // namespace safepower_agent
264
265
#endif  // PRODUCTION_BORG_MGMT_NODE_PROXY_SAFEPOWER_SAFEPOWER_AGENT_STATE_UPDATER_H_