def test_push_action(self): sharing_queue = SharingQueue() collection_name = 'collection_name' note_name = 'note_name' #update manifest file = open('../test_resources/XooML2.xml') update_manifest_action = UpdateSharedManifestAction(self.__account_id, collection_name, file) note_file = open('../test_resources/note2.xml') update_note_action = UpdateSharedNoteAction(self.__account_id, collection_name, note_name, note_file) img_file2 = open('../test_resources/note_img2.jpg') update_note_img_action = UpdateSharedNoteImageAction(self.__account_id, collection_name, note_name, img_file2) sharing_queue.push_action(update_manifest_action) sharing_queue.push_action(update_note_action) sharing_queue.push_action(update_note_img_action) expected_manifest_action = sharing_queue.pop_next_action().get_action_type() expected_note_action = sharing_queue.pop_next_action().get_action_type() expected_note_img_action = sharing_queue.pop_next_action().get_action_type() self.assertEqual(expected_manifest_action, SharingEvent.UPDATE_MANIFEST) self.assertEqual(expected_note_action, SharingEvent.UPDATE_NOTE) self.assertEqual(expected_note_img_action, SharingEvent.UPDATE_NOTE_IMG)
def __init__(self): self.__sharing_queue = SharingQueue() self.__sharing_queue.is_being_processed = False #primary listeners is a dictionary of user id to a request #these listeners are notified as soon as an update becomes #available for them self.__listeners = {} #backup listeners is a dictionary of user_id to a tuple of #(request, sharing_event). These listeners act as a backup #for primary listeners and save the next notification until #another listener is added for them. #As soon as another listener is added for these, the backup listeners #are notified with all the changes before the other listener arrived #and go back to the user. In this case, the second listener becomes a #backup listener self.__backup_listeners = {}
def test_push_and_pop_multiple_users(self): sharing_queue = SharingQueue() collection_name1 = 'collection_name1' collection_name2 = 'collection_name2' note_name1 = 'note_name' note_name2 = 'note_name2' action_list = [] #note actions #user1 note_file = open('../test_resources/note2.xml') for user_id in [self.__account_id, self.__subscriber_id]: #add the same note twice for x in range (2): update_note_action = UpdateSharedNoteAction(user_id, collection_name1, note_name1, note_file) action_list.append(update_note_action) #now add different notes for x in range(2): update_note_action = UpdateSharedNoteAction(user_id, collection_name1, note_name1 + str(x), note_file) action_list.append(update_note_action) #manifest actions manifest_file = open('../test_resources/XooML2.xml') for user_id in [self.__account_id, self.__subscriber_id]: #these should be added only once per user for x in range(5): update_manifest_action = UpdateSharedManifestAction(user_id, collection_name1, manifest_file) action_list.append(update_manifest_action) #image actions note_img = open('../test_resources/note2.xml') for user_id in [self.__account_id, self.__subscriber_id]: #these should be only added once per user for x in range (3): update_note_img_action = UpdateSharedNoteImageAction(user_id, collection_name1, note_name1, note_img) action_list.append(update_note_img_action) #now add different notes for x in range(3): update_note_img_action = UpdateSharedNoteImageAction(user_id, collection_name1, note_name1 + str(x), note_img) action_list.append(update_note_img_action) #now update the queue for action in action_list: sharing_queue.push_action(action) #verify #two manifest actions next_action = sharing_queue.pop_next_action() self.assertEqual(SharingEvent.UPDATE_MANIFEST, next_action.get_action_type()) next_action = sharing_queue.pop_next_action() self.assertEqual(SharingEvent.UPDATE_MANIFEST, next_action.get_action_type()) #6 update note actions next_action = sharing_queue.pop_next_action() self.assertEqual(SharingEvent.UPDATE_NOTE, next_action.get_action_type()) next_action = sharing_queue.pop_next_action() self.assertEqual(SharingEvent.UPDATE_NOTE, next_action.get_action_type()) next_action = sharing_queue.pop_next_action() self.assertEqual(SharingEvent.UPDATE_NOTE, next_action.get_action_type()) next_action = sharing_queue.pop_next_action() self.assertEqual(SharingEvent.UPDATE_NOTE, next_action.get_action_type()) next_action = sharing_queue.pop_next_action() self.assertEqual(SharingEvent.UPDATE_NOTE, next_action.get_action_type()) next_action = sharing_queue.pop_next_action() self.assertEqual(SharingEvent.UPDATE_NOTE, next_action.get_action_type()) #8 update img actions next_action = sharing_queue.pop_next_action() self.assertEqual(SharingEvent.UPDATE_NOTE_IMG, next_action.get_action_type()) next_action = sharing_queue.pop_next_action() self.assertEqual(SharingEvent.UPDATE_NOTE_IMG, next_action.get_action_type()) next_action = sharing_queue.pop_next_action() self.assertEqual(SharingEvent.UPDATE_NOTE_IMG, next_action.get_action_type()) next_action = sharing_queue.pop_next_action() self.assertEqual(SharingEvent.UPDATE_NOTE_IMG, next_action.get_action_type()) next_action = sharing_queue.pop_next_action() self.assertEqual(SharingEvent.UPDATE_NOTE_IMG, next_action.get_action_type()) next_action = sharing_queue.pop_next_action() self.assertEqual(SharingEvent.UPDATE_NOTE_IMG, next_action.get_action_type()) next_action = sharing_queue.pop_next_action() self.assertEqual(SharingEvent.UPDATE_NOTE_IMG, next_action.get_action_type()) next_action = sharing_queue.pop_next_action() self.assertEqual(SharingEvent.UPDATE_NOTE_IMG, next_action.get_action_type()) #rest should be empty next_action = sharing_queue.pop_next_action() self.assertTrue(next_action is None) next_action = sharing_queue.pop_next_action() self.assertTrue(next_action is None)
def test_pop_and_push_single_user(self): collection_name = 'col_name' sharing_queue = SharingQueue() popped_item = sharing_queue.pop_next_action() self.assertTrue(popped_item is None) #single push file = open('../test_resources/XooML2.xml') update_manifest_action = UpdateSharedManifestAction(self.__account_id, collection_name, file) sharing_queue.push_action(update_manifest_action) popped_item = sharing_queue.pop_next_action() self.assertTrue(popped_item is not None) popped_item = sharing_queue.pop_next_action() self.assertTrue(popped_item is None) #push and update sharing_queue.push_action(update_manifest_action) update_manifest_action = UpdateSharedManifestAction(self.__account_id, 'new_col_name', file) sharing_queue.push_action(update_manifest_action) popped_item = sharing_queue.pop_next_action() self.assertTrue(popped_item is not None) popped_item = sharing_queue.pop_next_action() self.assertTrue(popped_item is None) note_file = open('../test_resources/note2.xml') update_note_action = UpdateSharedNoteAction(self.__account_id, collection_name, 'note_1', note_file) note_file2 = open('../test_resources/note.xml') update_note_action2 = UpdateSharedNoteAction(self.__account_id, collection_name, 'note_2', note_file2) sharing_queue.push_action(update_manifest_action) sharing_queue.push_action(update_note_action) sharing_queue.push_action(update_note_action2) action = sharing_queue.pop_next_action() self.assertTrue(action is not None) action = sharing_queue.pop_next_action() self.assertTrue(action is not None) action = sharing_queue.pop_next_action() self.assertTrue(action is not None) action = sharing_queue.pop_next_action() self.assertTrue(action is None)
def test_is_empty_functionality_complex(self): sharing_queue = SharingQueue() collection_name1 = 'collection_name1' note_name1 = 'note_name' action_list = [] #note actions #user1 note_file = open('../test_resources/note2.xml') for user_id in [self.__account_id, self.__subscriber_id]: #add the same note twice for x in range (2): update_note_action = UpdateSharedNoteAction(user_id, collection_name1, note_name1, note_file) action_list.append(update_note_action) #now add different notes for x in range(2): update_note_action = UpdateSharedNoteAction(user_id, collection_name1, note_name1 + str(x), note_file) action_list.append(update_note_action) #manifest actions manifest_file = open('../test_resources/XooML2.xml') for user_id in [self.__account_id, self.__subscriber_id]: #these should be added only once per user for x in range(5): update_manifest_action = UpdateSharedManifestAction(user_id, collection_name1, manifest_file) action_list.append(update_manifest_action) #image actions note_img = open('../test_resources/note2.xml') for user_id in [self.__account_id, self.__subscriber_id]: #these should be only added once per user for x in range (3): update_note_img_action = UpdateSharedNoteImageAction(user_id, collection_name1, note_name1, note_img) action_list.append(update_note_img_action) #now add different notes for x in range(3): update_note_img_action = UpdateSharedNoteImageAction(user_id, collection_name1, note_name1 + str(x), note_img) action_list.append(update_note_img_action) #now update the queue for action in action_list: sharing_queue.push_action(action) #verify #two manifest actions next_action = sharing_queue.pop_next_action() self.assertEqual(SharingEvent.UPDATE_MANIFEST, next_action.get_action_type()) self.assertTrue(not sharing_queue.is_empty()) next_action = sharing_queue.pop_next_action() self.assertEqual(SharingEvent.UPDATE_MANIFEST, next_action.get_action_type()) self.assertTrue(not sharing_queue.is_empty()) #6 update note actions next_action = sharing_queue.pop_next_action() self.assertEqual(SharingEvent.UPDATE_NOTE, next_action.get_action_type()) self.assertTrue(not sharing_queue.is_empty()) next_action = sharing_queue.pop_next_action() self.assertEqual(SharingEvent.UPDATE_NOTE, next_action.get_action_type()) self.assertTrue(not sharing_queue.is_empty()) next_action = sharing_queue.pop_next_action() self.assertEqual(SharingEvent.UPDATE_NOTE, next_action.get_action_type()) self.assertTrue(not sharing_queue.is_empty()) next_action = sharing_queue.pop_next_action() self.assertEqual(SharingEvent.UPDATE_NOTE, next_action.get_action_type()) self.assertTrue(not sharing_queue.is_empty()) next_action = sharing_queue.pop_next_action() self.assertEqual(SharingEvent.UPDATE_NOTE, next_action.get_action_type()) self.assertTrue(not sharing_queue.is_empty()) next_action = sharing_queue.pop_next_action() self.assertEqual(SharingEvent.UPDATE_NOTE, next_action.get_action_type()) self.assertTrue(not sharing_queue.is_empty()) #8 update img actions next_action = sharing_queue.pop_next_action() self.assertEqual(SharingEvent.UPDATE_NOTE_IMG, next_action.get_action_type()) self.assertTrue(not sharing_queue.is_empty()) next_action = sharing_queue.pop_next_action() self.assertEqual(SharingEvent.UPDATE_NOTE_IMG, next_action.get_action_type()) self.assertTrue(not sharing_queue.is_empty()) next_action = sharing_queue.pop_next_action() self.assertEqual(SharingEvent.UPDATE_NOTE_IMG, next_action.get_action_type()) self.assertTrue(not sharing_queue.is_empty()) next_action = sharing_queue.pop_next_action() self.assertEqual(SharingEvent.UPDATE_NOTE_IMG, next_action.get_action_type()) self.assertTrue(not sharing_queue.is_empty()) next_action = sharing_queue.pop_next_action() self.assertEqual(SharingEvent.UPDATE_NOTE_IMG, next_action.get_action_type()) self.assertTrue(not sharing_queue.is_empty()) next_action = sharing_queue.pop_next_action() self.assertEqual(SharingEvent.UPDATE_NOTE_IMG, next_action.get_action_type()) self.assertTrue(not sharing_queue.is_empty()) next_action = sharing_queue.pop_next_action() self.assertEqual(SharingEvent.UPDATE_NOTE_IMG, next_action.get_action_type()) self.assertTrue(not sharing_queue.is_empty()) next_action = sharing_queue.pop_next_action() self.assertEqual(SharingEvent.UPDATE_NOTE_IMG, next_action.get_action_type()) self.assertTrue(sharing_queue.is_empty()) #rest should be empty next_action = sharing_queue.pop_next_action() self.assertTrue(next_action is None) self.assertTrue(sharing_queue.is_empty()) next_action = sharing_queue.pop_next_action() self.assertTrue(next_action is None) self.assertTrue(sharing_queue.is_empty()) sharing_queue.push_action(None) self.assertTrue(sharing_queue.is_empty()) #add some stuff back in #indexes 0 and 1 are the same action that should replace #each other sharing_queue.push_action(action_list[0]) self.assertTrue(not sharing_queue.is_empty()) sharing_queue.push_action(action_list[1]) self.assertTrue(not sharing_queue.is_empty()) sharing_queue.push_action(action_list[8]) self.assertTrue(not sharing_queue.is_empty()) sharing_queue.pop_next_action() self.assertTrue(not sharing_queue.is_empty()) sharing_queue.pop_next_action() self.assertTrue(sharing_queue.is_empty())
def test_is_empty_functionality_simple(self): collection_name = 'col_name' sharing_queue = SharingQueue() popped_item = sharing_queue.pop_next_action() self.assertTrue(popped_item is None) #single push file = open('../test_resources/XooML2.xml') update_manifest_action = UpdateSharedManifestAction(self.__account_id, collection_name, file) sharing_queue.push_action(update_manifest_action) self.assertTrue(not sharing_queue.is_empty()) sharing_queue.pop_next_action() self.assertTrue(sharing_queue.is_empty()) file2 = open('../test_resources/note.xml') sharing_queue.push_action(update_manifest_action) update_manifest_action2 = UpdateSharedManifestAction(self.__account_id, collection_name, file2) sharing_queue.push_action(update_manifest_action2) self.assertTrue(not sharing_queue.is_empty()) sharing_queue.pop_next_action() self.assertTrue(sharing_queue.is_empty())
class SharingSpaceController(SharingActionDelegate): """ A sharing space is per shared collection. """ __cache = MindcloudCache() __log = Log.log() #number of items to process in a batch #as the batch size grows actions will be processed faster however #there is a chance that we will be performing extra actions that #will be later replaced by a new action. #with a small batch size we process and execute slower however this #slowness allows us to see the next incoming actions and if necessary #replace an existing action in a queue by the new action #it seems that 5 is kinda our magic number any bigger and we have #weird lost packets in dropbox __BATCH_SIZE = MindcloudProperties.Properties.action_batch_size #current remaining actions in a batch __remaining_actions = 0 #At any point in time the following conditions may exist: # 1- There is a primary listener and no back up listener: # In this case the primary listener is notified and returned # to the user. Any other changes made during the time the user is # notified is lost. # 2- There is a primary listener and a backup listener: # In this case, as soon as a change happens the primary listener # gets notified and the backup listeners starts recording the changes # Once the user recieves the notification he should send back another # listener. As soon as another listener arrives the backup listener # returns to the user; the second listener becomes the backup listener # and begins recording all the changes while the user is notified. # If no changes happen during the first listener notifying the user # and backup listener recording. Then the second listener becomes # the primary listener and the backup listener stays a backup listener # 3- There is only a backup listener: # As mentioned in above the backup listener starts recording any thing # that happens while another listener is added. In that case the # backup listener returns to user with notification and the second # listener becomes the primary listener. __sharing_queue = None def __init__(self): self.__sharing_queue = SharingQueue() self.__sharing_queue.is_being_processed = False #primary listeners is a dictionary of user id to a request #these listeners are notified as soon as an update becomes #available for them self.__listeners = {} #backup listeners is a dictionary of user_id to a tuple of #(request, sharing_event). These listeners act as a backup #for primary listeners and save the next notification until #another listener is added for them. #As soon as another listener is added for these, the backup listeners #are notified with all the changes before the other listener arrived #and go back to the user. In this case, the second listener becomes a #backup listener self.__backup_listeners = {} def is_being_processed(self): if self.__sharing_queue is None: return False else: return self.__sharing_queue.is_being_processed def add_listener(self, user_id, request): """ Adds a listener to the list of the listeners. If the user is already existing , the listener will be added to the backup listeners. It is recommended that each user register a primary listener and a backup listener. The backup listener is never returned. This mechanism allows the user to alternate between primary and backup listener and always keep a listener. Args: user_id : The id of the user request : A tornado request object that will be returned to the user as response """ if user_id in self.__listeners: self.__log.info('SharingSpaceController - Backup listener added for user %s' % user_id) self.__backup_listeners[user_id] = (request, SharingEvent()) #if there is a backup listener for the current listener #check to see if it has updates elif user_id in self.__backup_listeners: backup_listener_events = self.__backup_listeners[user_id][1] if backup_listener_events.has_update(): self.__log.info('SharingSpaceController - backup listener has updates for user %s' % user_id) #return the back up listener to the user and make #the new listener backup listener backup_request = self.__backup_listeners[user_id][0] backup_request.write(backup_listener_events.convert_to_json_string()) backup_request.set_status(StorageResponse.OK) backup_request.finish() del self.__backup_listeners[user_id] self.__backup_listeners[user_id] = (request, SharingEvent()) self.__log.info('SharingSpaceController - backup listener update returned for user %s; replacing backup listener' % user_id) else: #There are no updates in the backup listener make this listener #the primary listener self.__listeners[user_id] = request self.__log.info('SharingSpaceController - primary listener added for user %s' % user_id) else: #the listener is not in primary listeners or backup listener #it must be the first listener add it to primary listerners self.__log.info('SharingSpaceController - primary listener added for user %s' % user_id) self.__listeners[user_id] = request def __finish_request(self, request): request.set_status(200) request.finish() def remove_listener(self, user_id): """ removes the primary and backup listener for the user if they exist """ if user_id in self.__backup_listeners: backup_listener = self.__backup_listeners[user_id][0] self.__finish_request(backup_listener) del self.__backup_listeners[user_id] if user_id in self.__listeners: primary_listener = self.__listeners[user_id] self.__finish_request(primary_listener) del self.__listeners[user_id] def cleanup(self): for user_id in self.__backup_listeners: backup_listener = self.__backup_listeners[user_id][0] self.__finish_request(backup_listener) del self.__backup_listeners[user_id] for user_id in self.__listeners: primary_listener = self.__listeners[user_id] self.__finish_request(primary_listener) del self.__listeners[user_id] def get_number_of_primary_listeners(self): """ Returns the number of primary listeners on this space """ return len(self.__listeners) def get_number_of_backup_listeners(self): """ Returns the number of backup listeners on this space. """ return len(self.__backup_listeners) def get_all_primary_listener_ids(self): """ Returns a list of user_id of the primary listeners """ return [user_id for user_id in self.__listeners] def get_all_backup_listener_ids(self): """ Returns a list of user_id of the backup listeners """ return [user_id for user_id in self.__backup_listeners] def add_action(self, sharing_action): """ An action that needs to be taken place and all the listeners sohuld get notified of When an action is added first all of the listeners get notified immediatley . Then the action goes on a queue and when the sharing space has time it will submit the action to the actual storage. However after registering an action and before submitting it, if a new action with the same type is registered. The latest action will take the place of the most recent one before it It is not neccessary for an action to affect only listeners. There might be an offline user that gets updated by the request. In those cases the timing for the submission of action is the based on the best try of the class. -Args: -``sharing_action``: A proper subclass of the sharing action """ #first notify all the listeners as fast as possible self.__notify_listeners(sharing_action) #Now add the action to the latest_sharing_actions to be #performed later. This is not as time bound as notify listeners #since the user has the perception of being real time self.__sharing_queue.push_action(sharing_action) #if the class is not processing the actions start processing them if not self.__sharing_queue.is_being_processed : self.__process_next_batch_of_queue_actions() def __process_next_batch_of_queue_actions(self): self.__sharing_queue.is_being_processed = True self.__process_actions_iterative(self.__BATCH_SIZE) def remove_temp_img(self, img_secret): """ There shouldn't be any real world use cases for this. The image will be automatically removed from cached based on the caching alg """ self.__cache.remove_temp_img(img_secret, callback=None) def __get_temp_img(self, img_secret, callback): """ Returns None in case its a cache miss """ self.__cache.get_temp_img(img_secret, callback) @gen.engine def get_temp_img(self, img_secret, user_id, collection_name, note_name=None, callback=None): """ Retrievs the temp img """ img = yield gen.Task(self.__get_temp_img, img_secret) #if its a cache miss; it has probably passed enough time to #get the image directly from the storage if img is None: #this relates to a note image if note_name is not None: img_file = yield gen.Task(StorageServer.get_note_image, user_id, collection_name, note_name) if img_file is not None: img = img_file.read() else: #its a thumbnail img_file = yield gen.Task(StorageServer.get_thumbnail, user_id, collection_name) if img_file is not None: img = img_file.read() if img is None: SharingSpaceController.__log.info('SharingSpaceController - failed to update img for %s; collection= %s; note=%s' % (user_id,collection_name,note_name)) callback(img) def __generate_img_secret(self, user_id, collection_name, note_name): return str(abs(hash(str(user_id+collection_name+note_name)))) @gen.engine def __store_temp_image(self, update_img_sharing_action, callback): """ store the img associated with the update image sharing action in the cache. """ user_id = update_img_sharing_action.get_user_id() collection_name = update_img_sharing_action.get_collection_name() note_name = 'thumbnail' if update_img_sharing_action.get_action_type() == SharingEvent.UPDATE_NOTE_IMG: note_name = update_img_sharing_action.get_note_name() img_file = update_img_sharing_action.get_associated_file() img_secret = self.__generate_img_secret(user_id, collection_name, note_name) update_img_sharing_action.set_img_secret(img_secret) self.__cache.set_temp_img(img_secret, img_file, callback=callback) @gen.engine def __notify_listeners(self, sharing_action): #for each primary listener notify the primary listener event_type = sharing_action.get_action_type() #in the case of the image we cache the image and notify the user #of the image, they can then request the temporary cached image if event_type == SharingEvent.UPDATE_NOTE_IMG or \ event_type == SharingEvent.UPDATE_THUMBNAIL: yield gen.Task(self.__store_temp_image, sharing_action) notified_listeners = set() for user_id in self.__listeners: if user_id != sharing_action.get_user_id() : request = self.__listeners[user_id] sharing_event = SharingEvent() sharing_event.add_event(sharing_action) notification_json = sharing_event.convert_to_json_string() request.write(notification_json) request.set_status(StorageResponse.OK) request.finish() SharingSpaceController.__log.info('SharingSpaceController - notified primary listener %s for sharing event %s - %s' % (user_id, sharing_action.get_action_type(), sharing_action.get_action_resource_name())) notified_listeners.add(user_id) #now update the backup listeners only for those items that #didn't get notified for user_id in self.__backup_listeners: #the backup listener didn't have a primary listener #so it must be in recording state if user_id not in notified_listeners and \ user_id != sharing_action.get_user_id(): backup_sharing_event = self.__backup_listeners[user_id][1] backup_sharing_event.add_event(sharing_action) SharingSpaceController.__log.info('SharingSpaceController - stored event for backup listener %s for sharing event %s - %s' % (user_id, sharing_action.get_action_type(), sharing_action.get_action_resource_name())) #delete notified user for user_id in notified_listeners: del self.__listeners[user_id] def __process_actions_iterative(self, iteration_count): if self.__sharing_queue.is_empty(): self.__sharing_queue.is_being_processed = False self.__log.info('SharingSpaceController - Finished executing all actions') return else: SharingSpaceController.__log.info('SharingSpaceController - Started processing batch of %s actions' % str(iteration_count)) actions_to_be_executed = [] for x in range(iteration_count): next_sharing_action = self.__sharing_queue.pop_next_action() if next_sharing_action is None: break #first increment the counters in remaining actions then execute them #this will make sure that when any action is being executed the counter #won't change else: self.__remaining_actions += 1 actions_to_be_executed.append(next_sharing_action) for action in actions_to_be_executed: SharingSpaceController.__log.info('SharingSpaceController - executing action %s' % action.name) action.execute(delegate=self) #delegate method from SharingActionDelegate @gen.engine def actionFinishedExecuting(self, action, response): """ This function is called when an action is finished executing. It keeps a count of the remaining actions and when it reaches zero it knows that the batch actions are finished and opens up the queue for second batch of processing -Args: -``action``: The SharingAction that got ginished executing -``remaining_actions``: The remaining actions to be finished before the batch of actions that action was part of is considered done """ #becauce of a dropbox bug it seems that some times files don't get submitted #this is not an eventual consistency problem, the files don't get submitted #even eventually. #we try to get the result of the just finished action and if it wasn't there #retry the action was_successful = yield gen.Task(action.was_successful) if was_successful: self.__remaining_actions -= 1 SharingSpaceController.__log.info('SharingSpaceController - finished executing action %s with response %s' % (action.name, str(response))) else: SharingSpaceController.__log.info('SharingSpaceController - Retrying action %s' % action.name) action.execute(delegate=self) if not self.__remaining_actions : SharingSpaceController.__log.info('Finished executing all the actions in the batch') self.__sharing_queue.is_being_processed = False self.__process_next_batch_of_queue_actions() def clear(self): self.__listeners.clear() self.__backup_listeners.clear() self.__sharing_queue.clear()