def test_asynchronous_ack_from_selectors(self): """ ack.on_next WaitOnLeftRight ------------> WaitOnLeftRight """ sink = TObserver(immediate_continue=0) left_sel_sink = TObserver(immediate_continue=0) right_sel_sink = TObserver(immediate_continue=0) obs = ControlledZipObservable( left=self.left, right=self.right, scheduler=self.scheduler, request_left=lambda left, right: left <= right, request_right=lambda left, right: right <= left, match_func=lambda left, right: left == right, ) obs.observe(init_observer_info(sink)) obs.left_selector.observe(init_observer_info(left_sel_sink)) obs.right_selector.observe(init_observer_info(right_sel_sink)) ack1 = AckSubject() ack2 = AckSubject() self.left.on_next_single(1).subscribe(ack1) self.right.on_next_single(1).subscribe(ack2) sink.ack.on_next(continue_ack) self.assertFalse(ack1.has_value) left_sel_sink.ack.on_next(continue_ack) self.assertTrue(ack1.has_value) self.assertFalse(ack2.has_value) right_sel_sink.ack.on_next(continue_ack) self.assertTrue(ack1.has_value) self.assertTrue(ack2.has_value)
def test_select_message_if_no_two_elements_match(self): """ ack.on_next WaitOnLeftRight ------------> WaitOnLeft """ sink = TObserver(immediate_continue=0) left_sel_sink = TObserver(immediate_continue=0) right_sel_sink = TObserver(immediate_continue=0) ack1 = AckSubject() ack2 = AckSubject() obs = ControlledZipObservable( left=self.left, right=self.right, scheduler=self.scheduler, request_left=lambda left, right: left <= right, request_right=lambda left, right: right <= left, match_func=lambda left, right: left == right, ) obs.observe(init_observer_info(sink)) obs.left_selector.observe(init_observer_info(left_sel_sink)) obs.right_selector.observe(init_observer_info(right_sel_sink)) self.left.on_next_single(1).subscribe(ack1) self.right.on_next_single(2).subscribe(ack2) self.assertIsInstance(self.measure_state(obs), ControlledZipStates.WaitOnLeft) self.assertIsInstance(left_sel_sink.received[0], SelectCompleted) self.left.on_next_single(2).subscribe(ack1) self.assertIsInstance(self.measure_state(obs), ControlledZipStates.WaitOnLeftRight) for instance_obj, class_obj in zip(left_sel_sink.received, [SelectCompleted, SelectNext, SelectCompleted]): self.assertIsInstance(instance_obj, class_obj)
def on_next(self, elem: ElementType): ack_subject = AckSubject() if isinstance(elem, list): elem = elem else: try: elem = list(elem) except Exception as exc: self.observer.on_error(exc) return stop_ack def action(_, __): inner_ack = self.observer.on_next(elem) if isinstance(inner_ack, ContinueAck): ack_subject.on_next(inner_ack) elif isinstance(inner_ack, StopAck): ack_subject.on_next(inner_ack) elif inner_ack is None: raise Exception( f'observer {self.observer} returned None instead of an Ack' ) else: inner_ack.subscribe(ack_subject) self.schedule_func(action) return ack_subject
def on_next(self, elem: ElementType): if self.back_pressure is None: if len(self.queue) < self.buffer_size: return_ack = continue_ack else: return_ack = AckSubject() self.back_pressure = return_ack else: return_ack = self.back_pressure with self.lock: len_queue = len(self.queue) self.queue.append(elem) prev_meas_state = self.state.get_measured_state(bool(len_queue)) if isinstance(prev_meas_state, BufferedStates.WaitingState): last_ack = prev_meas_state.last_ack self._start_loop(last_ack=last_ack, next_index=1) return return_ack elif isinstance(prev_meas_state, BufferedStates.RunningState): return return_ack else: return stop_ack
def gen_acks(): for subscription in subscriptions: # synchronize on_next call to conform with Observer convention with self.lock: ack = subscription.next_observer.on_next( materialized_values) if isinstance(ack, StopAck): with self.lock: try: self.subscriptions.remove(subscription) except ValueError: pass n_subscriptions = len(self.subscriptions) if n_subscriptions == 0: return stop_ack elif isinstance(ack, AckSubject): @dataclass class SubscriptionSingle(Single): subscriptions: List[ 'PublishObservableSubject.InnerSubscription'] subscription: 'PublishObservableSubject.InnerSubscription' out_ack: AckSubject lock: threading.RLock def on_next(self, elem): if isinstance(ack, StopAck): with self.lock: try: self.subscriptions.remove(subscription) except ValueError: pass n_subscriptions = len(self.subscriptions) if n_subscriptions == 0: self.out_ack.on_next(stop_ack) return else: self.out_ack.on_next(elem) out_ack = AckSubject() ack.subscribe( SubscriptionSingle( subscriptions=self.subscriptions, subscription=subscription, out_ack=out_ack, lock=self.lock, )) yield out_ack else: yield ack
class PromiseCounter: def __init__(self, value, initial): self.lock = threading.RLock() self.value = value self.counter = initial self.promise = AckSubject() def acquire(self): with self.lock: self.counter += 1 def countdown(self): with self.lock: self.counter -= 1 counter = self.counter if counter == 0: self.promise.on_next(self.value)
def on_next(self, elem: ElementType): self.on_next_counter += 1 try: values = list(elem) except Exception as exc: self.exception = exc return stop_ack self.received += values if self.immediate_continue is None: return continue_ack elif 0 < self.immediate_continue: self.immediate_continue -= 1 return continue_ack else: self.ack = AckSubject() return self.ack
def on_next(self, elem: ElementType): ack_subject = AckSubject() if isinstance(elem, list): elem = elem else: try: elem = list(elem) except Exception as exc: self.next_observer.on_error(exc) return def action(_, __): def subscribe_action(_, __): self.next_observer.on_next(elem) self.source_scheduler.schedule(subscribe_action) self.schedule_func(action) return ack_subject
def on_next(self, elem: ElementType): if not self.is_connected: def __(v): if isinstance(v, ContinueAck): ack = self.underlying.on_next(elem) return ack else: return StopAck() new_ack = AckSubject() # _merge_all(_map(_observe_on(self.connected_ack, scheduler=self.scheduler), func=__)).subscribe(new_ack) _merge_all(_map(self.connected_ack, func=__)).subscribe(new_ack) self.connected_ack = new_ack return self.connected_ack elif not self.was_canceled: ack = self.underlying.on_next(elem) return ack else: return stop_ack
def _iterate_over_batch( self, elem: ElementType, is_left: bool, ) -> Ack: """ This function is called once elements are received from left and right observable Loop over received elements. Send elements downstream if the match function applies. Request new elements from left, right or left and right observable. :param elem: either elements received form left or right observable depending on is_left argument :param is_left: if True then the _iterate_over_batch is called on left elements received :return: acknowledgment that will be returned from `on_next` called from either left or right observable """ upstream_ack = AckSubject() iterable = iter(elem) # empty iterable try: val = next(iterable) except StopIteration: return continue_ack next_state = RawControlledZipStates.ElementReceived( val=val, is_left=is_left, ack=upstream_ack, iter=iterable, ) with self.lock: next_state.prev_raw_state = self.state next_state.prev_raw_termination_state = self.termination_state self.state = next_state raw_prev_termination_state = next_state.prev_raw_termination_state prev_raw_state = next_state.prev_raw_state prev_state = prev_raw_state.get_measured_state( raw_prev_termination_state) if isinstance(prev_state, ControlledZipStates.Stopped): return stop_ack elif isinstance(prev_state, ControlledZipStates.WaitOnLeftRight): return upstream_ack elif is_left and isinstance(prev_state, ControlledZipStates.WaitOnLeft): left_val = val left_iter = iterable left_in_ack = upstream_ack right_val = prev_state.right_val right_iter = prev_state.right_iter right_in_ack = prev_state.right_ack other_upstream_ack = prev_state.right_ack elif not is_left and isinstance(prev_state, ControlledZipStates.WaitOnRight): left_val = prev_state.left_val left_iter = prev_state.left_iter left_in_ack = prev_state.left_ack right_val = val right_iter = iterable right_in_ack = upstream_ack other_upstream_ack = prev_state.left_ack else: raise Exception('unknown state "{}", is_left {}'.format( prev_state, is_left)) # keep elements to be sent in a buffer. Only when the incoming batch of elements is iterated over, the # elements in the buffer are sent. zipped_output_buffer = [] request_new_elem_from_left = False request_new_elem_from_right = False while True: # iterate over next series of SelectComplete (if exists) # break loop when encountering SelectNext or end of list while True: # collect SelectComplete, because they don't appear on the right side if isinstance(left_val, SelectCompleted): zipped_output_buffer.append(left_val) else: break try: left_val = next(left_iter) except StopIteration: request_new_elem_from_left = True break # break loop when there are no more right elements if request_new_elem_from_right or request_new_elem_from_left: break # left send SelectNext stop_right = False while True: if isinstance(right_val, SelectNext): # add to buffer zipped_output_buffer.append(left_val) elif isinstance(right_val, SelectCompleted): stop_right = True # always read next right value try: right_val = next(right_iter) except StopIteration: request_new_elem_from_right = True break if stop_right: break try: left_val = next(left_iter) except StopIteration: request_new_elem_from_left = True break # only send elements downstream, if there are any to be sent if zipped_output_buffer: zip_out_ack = self.observer.on_next(zipped_output_buffer) else: zip_out_ack = continue_ack # all elements in the left and right iterable are send downstream if request_new_elem_from_left and request_new_elem_from_right: next_state = RawControlledZipStates.WaitOnLeftRight() elif request_new_elem_from_left: next_state = RawControlledZipStates.WaitOnLeft( right_val=right_val, right_iter=right_iter, right_ack=right_in_ack, ) elif request_new_elem_from_right: next_state = RawControlledZipStates.WaitOnRight( left_val=left_val, left_iter=left_iter, left_ack=left_in_ack, ) else: raise Exception('at least one side should be back-pressured') with self.lock: # get termination state raw_prev_termination_state = self.termination_state # set next state self.state = next_state prev_termination_state = raw_prev_termination_state.get_measured_state( ) def stop_active_acks(): other_upstream_ack.on_next(stop_ack) # stop back-pressuring both sources, because there is no need to request elements # from completed source if isinstance(prev_termination_state, TerminationStates.LeftCompletedState) \ and request_new_elem_from_left: self._signal_on_complete_or_on_error(state=next_state) stop_active_acks() return stop_ack # stop back-pressuring both sources, because there is no need to request elements # from completed source elif isinstance(prev_termination_state, TerminationStates.RightCompletedState) \ and request_new_elem_from_right: self._signal_on_complete_or_on_error(state=next_state) stop_active_acks() return stop_ack # in error state, stop back-pressuring both sources elif isinstance(prev_termination_state, TerminationStates.ErrorState): self._signal_on_complete_or_on_error(state=next_state, ex=prev_termination_state.ex) stop_active_acks() return stop_ack # finish connecting ack only if not in Stopped or Error state else: if request_new_elem_from_left and request_new_elem_from_right: # directly return ack depending on whether left or right called `iterate_over_batch` if is_left: zip_out_ack.subscribe(right_in_ack) else: zip_out_ack.subscribe(left_in_ack) return zip_out_ack # all elements in the left buffer are send to the observer, back-pressure only left elif request_new_elem_from_left: if is_left: return zip_out_ack else: zip_out_ack.subscribe(left_in_ack) return right_in_ack # all elements in the left buffer are send to the observer, back-pressure only right elif request_new_elem_from_right: if is_left: zip_out_ack.subscribe(right_in_ack) return left_in_ack else: return zip_out_ack else: raise Exception('illegal case')
def __post_init__(self): self.root_ack = AckSubject() self.connected_ack = self.root_ack self.is_connected = False self.was_canceled = False self.is_volatile = False
class ConnectableObserver(Observer): underlying: Observer # scheduler: Scheduler # todo: remote this def __post_init__(self): self.root_ack = AckSubject() self.connected_ack = self.root_ack self.is_connected = False self.was_canceled = False self.is_volatile = False def connect(self): self.is_connected = True self.root_ack.on_next(continue_ack) def on_next(self, elem: ElementType): if not self.is_connected: def __(v): if isinstance(v, ContinueAck): ack = self.underlying.on_next(elem) return ack else: return StopAck() new_ack = AckSubject() # _merge_all(_map(_observe_on(self.connected_ack, scheduler=self.scheduler), func=__)).subscribe(new_ack) _merge_all(_map(self.connected_ack, func=__)).subscribe(new_ack) self.connected_ack = new_ack return self.connected_ack elif not self.was_canceled: ack = self.underlying.on_next(elem) return ack else: return stop_ack def on_error(self, err): self.was_canceled = True if not self.is_connected: class ResultSingle(Single): def on_next(_, v): if isinstance(v, ContinueAck): self.underlying.on_error(err) # _observe_on(self.connected_ack, scheduler=self.scheduler).subscribe(ResultSingle()) self.connected_ack.subscribe(ResultSingle()) else: self.underlying.on_error(err) def on_completed(self): if not self.is_connected: class ResultSingle(Single): def on_next(_, v): if isinstance(v, ContinueAck): self.underlying.on_completed() # _observe_on(self.connected_ack, scheduler=self.scheduler).subscribe(ResultSingle()) self.connected_ack.subscribe(ResultSingle()) else: self.underlying.on_completed()
def _iterate_over_batch(self, elem: ElementType, is_left: bool): """ this function is called on `on_next` call from left or right observable """ # if elem is a list, make an iterator out of it iterable = iter(elem) # in case the zip process is started and the output observer returns a synchronous acknowledgment, # then `upstream_ack` is not actually needed; nevertheless, it is created here, because it makes # the code simpler upstream_ack = AckSubject() # prepare next raw state next_state = RawZipStates.ElementReceived( is_left=is_left, ack=upstream_ack, iter=iterable, ) # synchronous update the state with self.lock: next_state.prev_raw_state = self.state next_state.prev_raw_termination_state = self.termination_state self.state = next_state meas_state = next_state.get_measured_state( next_state.prev_raw_termination_state) # pattern match measured state if isinstance(meas_state, ZipStates.Stopped): return stop_ack # wait on other observable elif isinstance(meas_state, ZipStates.WaitOnRight) or isinstance( meas_state, ZipStates.WaitOnLeft): return upstream_ack # start zipping operation elif isinstance(meas_state, ZipStates.ZipElements): if is_left: other_upstream_ack = meas_state.right_ack else: other_upstream_ack = meas_state.left_ack else: raise Exception(f'unknown state "{meas_state}", is_left {is_left}') # in case left and right batch don't match in number of elements, # n1 will not be None after zipping n1 = [None] def gen_zipped_elements(): """ generate a sequence of zipped elements """ while True: n1[0] = None try: n1[0] = next(meas_state.left_iter) n2 = next(meas_state.right_iter) except StopIteration: break # yield self.selector(n1[0], n2) yield (n1[0], n2) try: # zip left and right batch zipped_elements = list(gen_zipped_elements()) except Exception as exc: # self.observer.on_error(exc) other_upstream_ack.on_next(stop_ack) # return stop_ack raise Exception( to_operator_exception( message='', stack=self.stack, )) if 0 < len(zipped_elements): downstream_ack = self.observer.on_next(zipped_elements) else: downstream_ack = continue_ack if isinstance(downstream_ack, StopAck): other_upstream_ack.on_next(stop_ack) return stop_ack # request new element from left source if n1[0] is None: new_left_iter = None request_new_elem_from_left = True # request new element also from right source? try: val = next(meas_state.right_iter) new_right_iter = itertools.chain([val], meas_state.right_iter) request_new_elem_from_right = False # request new element from left and right source except StopIteration: new_right_iter = None request_new_elem_from_right = True # request new element only from right source else: new_left_iter = itertools.chain(n1, meas_state.left_iter) new_right_iter = None request_new_elem_from_left = False request_new_elem_from_right = True # define next state after zipping # ------------------------------- # request new element from both sources if request_new_elem_from_left and request_new_elem_from_right: next_state = RawZipStates.WaitOnLeftRight() # request new element only from right source elif request_new_elem_from_right: next_state = RawZipStates.WaitOnRight( left_iter=new_left_iter, left_ack=meas_state.left_ack, ) # request new element only from left source elif request_new_elem_from_left: next_state = RawZipStates.WaitOnLeft( right_iter=new_right_iter, right_ack=meas_state.right_ack, ) else: raise Exception('after the zip operation, a new element needs ' 'to be requested from at least one source') with self.lock: # get termination state raw_prev_termination_state = self.termination_state # set next state self.state = next_state meas_state = next_state.get_measured_state(raw_prev_termination_state) # stop zip observable # previous state cannot be "Stopped", therefore don't check previous state if isinstance(meas_state, ZipStates.Stopped): prev_termination_state = raw_prev_termination_state.get_measured_state( ) if isinstance(prev_termination_state, TerminationStates.ErrorState): self.observer.on_error(prev_termination_state.ex) other_upstream_ack.on_next(stop_ack) return stop_ack else: self.observer.on_completed() other_upstream_ack.on_next(stop_ack) return stop_ack # request new elements else: if request_new_elem_from_left and request_new_elem_from_right: downstream_ack.subscribe(other_upstream_ack) return downstream_ack elif request_new_elem_from_right: if is_left: downstream_ack.subscribe(other_upstream_ack) else: return downstream_ack elif request_new_elem_from_left: if is_left: return downstream_ack else: downstream_ack.subscribe(other_upstream_ack) else: raise Exception('at least one side should be back-pressured') return upstream_ack
def __init__(self, value, initial): self.lock = threading.RLock() self.value = value self.counter = initial self.promise = AckSubject()
def _iterate_over_batch( self, elem: ElementType, is_left: bool, ) -> Ack: """ This function is called once elements are received from left and right observable Loop over received elements. Send elements downstream if the match function applies. Request new elements from left, right or left and right observable. :param elem: either elements received form left or right observable depending on is_left argument :param is_left: if True then the _iterate_over_batch is called on left elements received :return: acknowledgment that will be returned from `on_next` called from either left or right observable """ upstream_ack = AckSubject() iterable = iter(elem) # empty iterable try: val = next(iterable) except StopIteration: return continue_ack next_state = RawControlledZipStates.ElementReceived( val=val, is_left=is_left, ack=upstream_ack, iter=iterable, ) with self.lock: next_state.prev_raw_state = self.state next_state.prev_raw_termination_state = self.termination_state self.state = next_state raw_prev_termination_state = next_state.prev_raw_termination_state prev_raw_state = next_state.prev_raw_state prev_state = prev_raw_state.get_measured_state( raw_prev_termination_state) if isinstance(prev_state, ControlledZipStates.Stopped): return stop_ack elif isinstance(prev_state, ControlledZipStates.WaitOnLeftRight): return upstream_ack elif is_left and isinstance(prev_state, ControlledZipStates.WaitOnLeft): left_val = val left_iter = iterable left_in_ack = upstream_ack # last_left_sel_ack = None right_val = prev_state.right_val right_iter = prev_state.right_iter right_in_ack = prev_state.right_ack # last_right_sel_ack = prev_state.right_sel_ack other_upstream_ack = prev_state.right_ack elif not is_left and isinstance(prev_state, ControlledZipStates.WaitOnRight): left_val = prev_state.left_val left_iter = prev_state.left_iter left_in_ack = prev_state.left_ack # last_left_sel_ack = prev_state.left_sel_ack right_val = val right_iter = iterable right_in_ack = upstream_ack # last_right_sel_ack = None other_upstream_ack = prev_state.left_ack else: raise Exception('unknown state "{}", is_left {}'.format( prev_state, is_left)) # keep elements to be sent in a buffer. Only when the incoming batch of elements is iterated over, the # elements in the buffer are sent. left_index_buffer = [ ] # index of the elements from the left observable that got selected right_index_buffer = [] # by the match function zipped_output_buffer = [] request_new_elem_from_left = False request_new_elem_from_right = False # iterate over two batches of elements from left and right observable # until one batch is empty while True: if self.match_func(left_val, right_val): left_index_buffer.append(select_next) right_index_buffer.append(select_next) # add to buffer zipped_output_buffer.append((left_val, right_val)) request_left = self.request_left(left_val, right_val) request_right = self.request_right(left_val, right_val) if request_left: left_index_buffer.append(select_completed) try: left_val = next(left_iter) except StopIteration: request_new_elem_from_left = True if request_right: right_index_buffer.append(select_completed) try: right_val = next(right_iter) except StopIteration: request_new_elem_from_right = True break elif not request_left: raise Exception( 'either `request_left` or `request_right` condition needs to be True' ) if request_new_elem_from_left: break # only send elements downstream, if there are any to be sent if zipped_output_buffer: zip_out_ack = self.observer.on_next(zipped_output_buffer) else: zip_out_ack = continue_ack # only send elements over the left selector observer, if there are any to be sent if left_index_buffer: left_out_ack = self.left_selector.on_next(left_index_buffer) else: left_out_ack = continue_ack #last_left_sel_ack or continue_ack # only send elements over the right selector observer, if there are any to be sent if right_index_buffer: right_out_ack = self.right_selector.on_next(right_index_buffer) else: right_out_ack = continue_ack #last_right_sel_ack or continue_ack # all elements in the left and right iterable are send downstream if request_new_elem_from_left and request_new_elem_from_right: next_state = RawControlledZipStates.WaitOnLeftRight( # right_sel=right_sel, # left_sel=left_sel, ) elif request_new_elem_from_left: next_state = RawControlledZipStates.WaitOnLeft( right_val=right_val, right_iter=right_iter, right_ack=right_in_ack, # right_sel=right_sel, # left_sel=left_sel, # right_sel_ack=right_out_ack, ) elif request_new_elem_from_right: next_state = RawControlledZipStates.WaitOnRight( left_val=left_val, left_iter=left_iter, left_ack=left_in_ack, # right_sel=right_sel, # left_sel=left_sel, # left_sel_ack=left_out_ack, ) else: raise Exception('at least one side should be back-pressured') with self.lock: # get termination state raw_prev_termination_state = self.termination_state # set next state self.state = next_state prev_termination_state = raw_prev_termination_state.get_measured_state( ) def stop_active_acks(): # if isinstance(last_right_sel_ack, AckSubject): # last_right_sel_ack.on_next(stop_ack) # elif isinstance(last_left_sel_ack, AckSubject): # last_left_sel_ack.on_next(stop_ack) other_upstream_ack.on_next(stop_ack) # def merge_ack(ack1: Ack, ack2: Ack) -> Ack: # if isinstance(ack2, ContinueAck) or isinstance(ack1, StopAck): # return_ack = ack1 # # elif isinstance(ack2, StopAck): # return ack2 # # elif isinstance(ack1, ContinueAck): # return_ack = _map(ack2, lambda _: continue_ack) # # else: # return_ack = _map(source=_zip(ack1, ack2), func=lambda t2: t2[0]) # # return return_ack # stop back-pressuring both sources, because there is no need to request elements # from completed source if isinstance(prev_termination_state, TerminationStates.LeftCompletedState) \ and request_new_elem_from_left: self._signal_on_complete_or_on_error(state=next_state) stop_active_acks() return stop_ack # stop back-pressuring both sources, because there is no need to request elements # from completed source elif isinstance(prev_termination_state, TerminationStates.RightCompletedState) \ and request_new_elem_from_right: self._signal_on_complete_or_on_error(state=next_state) stop_active_acks() return stop_ack # in error state, stop back-pressuring both sources elif isinstance(prev_termination_state, TerminationStates.ErrorState): self._signal_on_complete_or_on_error(state=next_state, exc=prev_termination_state.ex) stop_active_acks() return stop_ack # finish connecting ack only if not in Stopped or Error state else: if request_new_elem_from_left and request_new_elem_from_right: # integrate selector acks result_ack_left = merge_ack(zip_out_ack, left_out_ack) result_ack_right = merge_ack(zip_out_ack, right_out_ack) # directly return ack depending on whether left or right called `iterate_over_batch` if is_left: result_ack_right.subscribe(right_in_ack) return result_ack_left else: result_ack_left.subscribe(left_in_ack) return result_ack_right # all elements in the left buffer are send to the observer, back-pressure only left elif request_new_elem_from_left: result_out_ack = merge_ack( merge_ack(zip_out_ack, left_out_ack), right_out_ack) if is_left: return result_out_ack else: result_out_ack.subscribe(left_in_ack) return right_in_ack # all elements in the left buffer are send to the observer, back-pressure only right elif request_new_elem_from_right: result_out_ack = merge_ack( merge_ack(zip_out_ack, left_out_ack), right_out_ack) if is_left: result_out_ack.subscribe(right_in_ack) return left_in_ack else: return result_out_ack else: raise Exception('illegal case')
def on_next(self, elem: ElementType): next_state = RawMergeStates.OnRightReceived(elem=elem, ack=AckSubject()) return source._on_next(next_state)
def on_next(self, outer_elem: ElementType): if isinstance(outer_elem, list): outer_vals = outer_elem else: try: # materialize received values immediately outer_vals = list(outer_elem) except Exception as exc: self.on_error(exc) return stop_ack if len(outer_vals) == 0: return continue_ack # the ack that might be returned by this `on_next` async_upstream_ack = AckSubject() def subscribe_action(_, __): conn_observer = None for val in reversed(outer_vals[1:]): # create a new `InnerObserver` for each inner observable inner_observer = FlatMapInnerObserver( observer_info=self.observer_info, next_conn_observer=conn_observer, outer_upstream_ack=async_upstream_ack, state=self.state, lock=self.lock, ) # add ConnectableObserver to observe all inner observables simultaneously, and # to get control of activating one after the other conn_observer = ConnectableObserver( underlying=inner_observer, ) # apply selector to get inner observable (per element received) try: inner_observable = self.func(val) # for mypy to type check correctly assert isinstance(self.observer_info, ObserverInfo) # observe inner observable disposable = inner_observable.observe( self.observer_info.copy(observer=conn_observer, )) self.composite_disposable.add(disposable) except Exception as exc: self.on_error(exc) return stop_ack # create a new `InnerObserver` for each inner observable inner_observer = FlatMapInnerObserver( observer_info=self.observer_info, next_conn_observer=conn_observer, outer_upstream_ack=async_upstream_ack, state=self.state, lock=self.lock, ) next_state = RawFlatMapStates.Active() with self.lock: next_state.raw_prev_state = self.state[0] self.state[0] = next_state meas_state = next_state.raw_prev_state.get_measured_state() if isinstance(meas_state, FlatMapStates.Stopped): return stop_ack # for mypy to type check correctly assert isinstance(self.observer_info, ObserverInfo) try: # apply selector to get inner observable (per element received) inner_observable = self.func(outer_vals[0]) # observe inner observable disposable = inner_observable.observe( self.observer_info.copy(observer=inner_observer, )) self.composite_disposable.add(disposable) except Exception as exc: self.observer_info.observer.on_error(exc) return stop_ack if self.subscribe_scheduler.idle: disposable = self.subscribe_scheduler.schedule(subscribe_action) self.composite_disposable.add(disposable) else: subscribe_action(None, None) return async_upstream_ack