88)
99from ops .model import ModelError
1010
11- from .base import LBBase
11+ from .base import VersionedInterface
1212
1313
1414class LBProviderAvailable (EventBase ):
@@ -24,7 +24,7 @@ class LBProviderEvents(ObjectEvents):
2424 responses_changed = EventSource (LBResponsesChanged )
2525
2626
27- class LBProvider (LBBase ):
27+ class LBProvider (VersionedInterface ):
2828 """ API used to interact with the provider of loadbalancers.
2929 """
3030 state = StoredState ()
@@ -52,15 +52,20 @@ def _check_provider(self, event):
5252 self .on .available .emit ()
5353 if self .is_changed :
5454 self .on .responses_changed .emit ()
55- else :
55+ elif self . state . was_available :
5656 self .state .was_available = False
57+ if self .state .response_hashes :
58+ self .state .response_hashes = {}
59+ self .on .responses_changed .emit ()
5760
5861 @property
5962 def relation (self ):
6063 return self .relations [0 ] if self .relations else None
6164
6265 def get_request (self , name ):
6366 """ Get or create a Load Balancer Request object.
67+
68+ May raise a ModelError if unable to create a request.
6469 """
6570 if not self .charm .unit .is_leader ():
6671 raise ModelError ('Unit is not leader' )
@@ -82,42 +87,105 @@ def get_request(self, name):
8287 def get_response (self , name ):
8388 """ Get a specific Load Balancer Response by name.
8489
85- This is equivalent to `get_request(name).response`.
90+ This is equivalent to `get_request(name).response`, except that it
91+ will return `None` if the response is not available.
8692 """
87- return self .get_request (name ).response
93+ if not self .is_available :
94+ return None
95+ request = self .get_request (name )
96+ if not request .response :
97+ return None
98+ return request .response
8899
89100 def send_request (self , request ):
90101 """ Send a specific request.
102+
103+ May raise a ModelError if unable to send the request.
91104 """
105+ if not self .charm .unit .is_leader ():
106+ raise ModelError ('Unit is not leader' )
107+ if not self .relation :
108+ raise ModelError ('Relation not available' )
109+ # The sent_hash is used to tell when the provider's response has been
110+ # updated to match our request. We can't used the request hash computed
111+ # on the providing side because it may not match due to default values
112+ # being filled in on that side (e.g., the backend addresses).
113+ request .sent_hash = request .hash
92114 key = 'request_' + request .name
93115 self .relation .data [self .app ][key ] = request .dumps ()
94- self .state .response_hashes [request .name ] = None
116+
117+ def remove_request (self , name ):
118+ """ Remove a specific request.
119+
120+ May raise a ModelError if unable to remove the request.
121+ """
122+ if not self .charm .unit .is_leader ():
123+ raise ModelError ('Unit is not leader' )
124+ if not self .relation :
125+ return
126+ key = 'request_' + name
127+ self .relation .data [self .app ].pop (key , None )
128+ self .state .response_hashes .pop (name , None )
129+
130+ @property
131+ def all_requests (self ):
132+ """ A list of all requests which have been made.
133+ """
134+ requests = []
135+ if self .relation :
136+ for key in sorted (self .relation .data [self .app ].keys ()):
137+ if not key .startswith ('request_' ):
138+ continue
139+ requests .append (self .get_request (key [len ('request_' ):]))
140+ return requests
141+
142+ @property
143+ def revoked_responses (self ):
144+ """ A list of responses which are no longer available.
145+ """
146+ return [request .response
147+ for request in self .all_requests
148+ if not request .response
149+ and request .name in self .state .response_hashes ]
95150
96151 @cached_property
97152 def all_responses (self ):
98153 """ A list of all responses which are available.
99154 """
100- local_data = self .relation .data [self .app ]
101- names = [key [len ('request_' ):]
102- for key in local_data .keys ()
103- if key .startswith ('request_' )]
104- requests = [self .get_request (name ) for name in names ]
105- return [request .response for request in requests if request .response ]
155+ return [request .response
156+ for request in self .all_requests
157+ if request .response ]
158+
159+ @cached_property
160+ def complete_responses (self ):
161+ """ A list of all responses which are up to date with their associated
162+ request.
163+ """
164+ return [request .response
165+ for request in self .all_requests
166+ if request .response .received_hash == request .sent_hash ]
106167
107168 @property
108169 def new_responses (self ):
109- """ A list of all responses which have not yet been acknowledged as
170+ """ A list of complete responses which have not yet been acknowledged as
110171 handled or which have changed.
111172 """
173+ acked_responses = self .state .response_hashes
112174 return [response
113- for response in self .all_responses
114- if self . state . response_hashes [ response .name ] != response .hash ]
175+ for response in self .complete_responses
176+ if response .hash != acked_responses . get ( response .name ) ]
115177
116178 def ack_response (self , response ):
117179 """ Acknowledge that a given response has been handled.
180+
181+ Can be called on a revoked response as well to remove it
182+ from the revoked_responses list.
118183 """
119- self .state .response_hashes [response .name ] = response .hash
120- if not self .new_responses :
184+ if response :
185+ self .state .response_hashes [response .name ] = response .hash
186+ else :
187+ self .state .response_hashes .pop (response .name , None )
188+ if not self .is_changed :
121189 try :
122190 from charms .reactive import clear_flag
123191 prefix = 'endpoint.' + self .relation_name
@@ -127,12 +195,16 @@ def ack_response(self, response):
127195
128196 @property
129197 def is_changed (self ):
130- return bool ( self .new_responses )
198+ return self .new_responses or self . revoked_responses
131199
132200 @property
133201 def is_available (self ):
134202 return bool (self .relation )
135203
204+ @property
205+ def can_request (self ):
206+ return self .is_available and self .unit .is_leader ()
207+
136208 def manage_flags (self ):
137209 """ Used to interact with charms.reactive-base charms.
138210 """
0 commit comments