Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for dropping messages. #16

Merged
merged 3 commits into from Feb 9, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion db/dummy_data.sql
@@ -1,5 +1,5 @@
LOCK TABLES `mode` WRITE;
INSERT INTO `mode` VALUES (26,'call'),(35,'email'),(17,'im'),(8,'sms');
INSERT INTO `mode` VALUES (26,'call'),(35,'email'),(17,'im'),(8,'sms'),(36,'drop');
UNLOCK TABLES;

LOCK TABLES `priority` WRITE;
Expand Down Expand Up @@ -92,3 +92,5 @@ UNLOCK TABLES;

LOCK TABLES `user_team` WRITE;
UNLOCK TABLES;

INSERT IGNORE INTO `application_mode` (`application_id`, `mode_id`) SELECT `application`.`id`, `mode`.`id` FROM `application`, `mode` WHERE `mode`.`name` != 'drop';
22 changes: 18 additions & 4 deletions db/schema_0.sql
Expand Up @@ -578,15 +578,29 @@ CREATE TABLE `application_quota` (
`soft_quota_threshold` smallint(5) NOT NULL,
`hard_quota_duration` smallint(5) NOT NULL,
`soft_quota_duration` smallint(5) NOT NULL,
`plan_name` varchar(255) NOT NULL,
`target_id` bigint(20) NOT NULL,
`plan_name` varchar(255),
`target_id` bigint(20),
`wait_time` smallint(5) NOT NULL DEFAULT 0,
PRIMARY KEY (`application_id`),
KEY `application_quota_plan_name_fk_idx` (`plan_name`),
KEY `application_quota_target_id_fk_idx` (`target_id`),
CONSTRAINT `application_id_ibfk` FOREIGN KEY (`application_id`) REFERENCES `application` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `plan_name_ibfk` FOREIGN KEY (`plan_name`) REFERENCES `plan_active` (`name`) ON DELETE NO ACTION ON UPDATE CASCADE,
CONSTRAINT `target_id_ibfk` FOREIGN KEY (`target_id`) REFERENCES `target` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
CONSTRAINT `plan_name_ibfk` FOREIGN KEY (`plan_name`) REFERENCES `plan_active` (`name`) ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT `target_id_ibfk` FOREIGN KEY (`target_id`) REFERENCES `target` (`id`) ON DELETE SET NULL ON UPDATE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;


--
-- Table structure for table `application_mode`
--

DROP TABLE IF EXISTS `application_mode`;
CREATE TABLE `application_mode` (
`application_id` int(11) NOT NULL,
`mode_id` int(11) NOT NULL,
PRIMARY KEY (`application_id`, `mode_id`),
CONSTRAINT `application_mode_application_id_ibfk` FOREIGN KEY (`application_id`) REFERENCES `application` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `application_mode_mode_id_ibfk` FOREIGN KEY (`mode_id`) REFERENCES `mode` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=latin1;


Expand Down
22 changes: 19 additions & 3 deletions src/iris_api/api.py
Expand Up @@ -410,6 +410,13 @@
JOIN `application` on `application`.`id` = `default_application_mode`.`application_id`
WHERE `application`.`name` = %s'''

get_supported_application_modes_query = '''
SELECT `mode`.`name`
FROM `mode`
JOIN `application_mode` on `mode`.`id` = `application_mode`.`mode_id`
WHERE `application_mode`.`application_id` = %s
'''

insert_user_modes_query = '''INSERT
INTO `target_mode` (`priority_id`, `target_id`, `mode_id`)
VALUES (
Expand Down Expand Up @@ -1437,6 +1444,9 @@ def on_get(self, req, resp, app_name):
cursor.execute(get_default_application_modes_query, app_name)
app['default_modes'] = {row['priority']: row['mode'] for row in cursor}

cursor.execute(get_supported_application_modes_query, app['id'])
app['supported_modes'] = [row['name'] for row in cursor]

cursor.close()
connection.close()

Expand Down Expand Up @@ -1480,8 +1490,13 @@ def on_get(self, req, resp):
app['variables'].append(row['name'])
if row['required']:
app['required_variables'].append(row['name'])

cursor.execute(get_default_application_modes_query, app['name'])
app['default_modes'] = {row['priority']: row['mode'] for row in cursor}

cursor.execute(get_supported_application_modes_query, app['id'])
app['supported_modes'] = [row['name'] for row in cursor]

del app['id']
payload = apps
cursor.close()
Expand All @@ -1496,12 +1511,13 @@ class Modes(object):
def on_get(self, req, resp):
connection = db.engine.raw_connection()
cursor = connection.cursor(db.dict_cursor)
mode_query = 'SELECT `id`,`name` FROM `mode`'''
# Deliberately omit "drop" as it's a special case only supported in very limited circumstances and shouldn't
# be thrown all over the UI
mode_query = 'SELECT `name` FROM `mode` WHERE `name` != "drop"'
cursor.execute(mode_query)
results = cursor.fetchall()
payload = [r['name'] for r in cursor]
cursor.close()
connection.close()
payload = [r['name'] for r in results]
resp.status = HTTP_200
resp.body = ujson.dumps(payload)

Expand Down
139 changes: 90 additions & 49 deletions src/iris_api/bin/sender.py
Expand Up @@ -597,57 +597,74 @@ def set_target_fallback_mode(message):
return True
# target doesn't have email either - bail
except ValueError:
logger.error('target does not have mode(%s) %r', target_fallback_mode, message)
logger.exception('target does not have mode(%s) %r', target_fallback_mode, message)
message['destination'] = message['mode'] = message['mode_id'] = None
return False


def set_target_contact_by_priority(message):
session = db.Session()
result = session.execute('''
SELECT `destination`, `mode`.`name`, `mode`.`id`
FROM `target` JOIN `target_contact` ON `target_contact`.`target_id` = `target`.`id`
JOIN `mode` ON `mode`.`id` = `target_contact`.`mode_id`
WHERE `target`.`name` = :target AND `target_contact`.`mode_id` = IFNULL(
-- 1. lookup per application user setting
(
SELECT `target_application_mode`.`mode_id`
FROM `target_application_mode`
JOIN `application` ON `target_application_mode`.`application_id` = `application`.`id`
WHERE `target_application_mode`.`target_id` = `target`.`id` AND
`application`.`name` = :application AND
`target_application_mode`.`priority_id` = :priority_id
), IFNULL(
-- 2. Lookup default setting for this app
(
SELECT `default_application_mode`.`mode_id`
FROM `default_application_mode`
JOIN `application` ON `default_application_mode`.`application_id` = `application`.`id`
WHERE `default_application_mode`.`priority_id` = :priority_id AND
`application`.`name` = :application
), IFNULL(
-- 3. lookup default user setting
(
SELECT `target_mode`.`mode_id`
FROM `target_mode`
WHERE `target_mode`.`target_id` = `target`.`id` AND
`target_mode`.`priority_id` = :priority_id
), (
-- 4. lookup default iris setting
SELECT `mode_id`
FROM `priority`
WHERE `id` = :priority_id
)
SELECT `target_contact`.`destination` AS dest, `mode`.`name` AS mode_name, `mode`.`id` AS mode_id
FROM `mode`
JOIN `target` ON `target`.`name` = :target
JOIN `application` ON `application`.`name` = :application

-- left join because the "drop" mode isn't going to have a target_contact entry
LEFT JOIN `target_contact` ON `target_contact`.`mode_id` = `mode`.`id` AND `target_contact`.`target_id` = `target`.`id`

WHERE mode.id = IFNULL((
SELECT `target_application_mode`.`mode_id`
FROM `target_application_mode`
WHERE `target_application_mode`.`target_id` = target.id AND
`target_application_mode`.`application_id` = `application`.`id` AND
`target_application_mode`.`priority_id` = :priority_id
), IFNULL(
-- 2. Lookup default setting for this app
(
SELECT `default_application_mode`.`mode_id`
FROM `default_application_mode`
WHERE `default_application_mode`.`priority_id` = :priority_id AND
`default_application_mode`.`application_id` = `application`.`id`
), IFNULL(
-- 3. lookup default user setting
(
SELECT `target_mode`.`mode_id`
FROM `target_mode`
WHERE `target_mode`.`target_id` = target.id AND
`target_mode`.`priority_id` = :priority_id
), (
-- 4. lookup default iris setting
SELECT `mode_id`
FROM `priority`
WHERE `id` = :priority_id
)
)
)
)
)
)''', message)
[(destination, mode, mode_id)] = result
session.close()
-- Make sure this mode is allowed for this application. Eg important apps can't drop.
AND EXISTS (SELECT 1 FROM `application_mode`
WHERE `application_mode`.`mode_id` = `mode`.`id`
AND `application_mode`.`application_id` = `application`.`id`)
''', message)

try:
[(destination, mode, mode_id)] = result
except ValueError:
raise
finally:
session.close()

if not destination and mode != 'drop':
logger.error('Did not find destination for message %s and mode is not drop', message)
return False

message['destination'] = destination
message['mode'] = mode
message['mode_id'] = mode_id

return True


def set_target_contact(message):
try:
Expand All @@ -664,13 +681,14 @@ def set_target_contact(message):
message['destination'] = cursor.fetchone()[0]
cursor.close()
connection.close()
result = True
else:
# message triggered by incident will only have priority
set_target_contact_by_priority(message)
result = set_target_contact_by_priority(message)
cache.target_reprioritization(message)
return True
return result
except ValueError:
logger.error('target does not have mode %r', message)
logger.exception('target does not have mode %r', message)
return set_target_fallback_mode(message)


Expand Down Expand Up @@ -704,7 +722,8 @@ def render(message):
try:
application_template = template[message['application']]
try:
mode_template = application_template[message['mode']]
# When we want to "render" a dropped message, treat it as if it's an email
mode_template = application_template['email' if message['mode'] == 'drop' else message['mode']]
try:
message['subject'] = mode_template['subject'].render(**message['context'])
except Exception as e:
Expand Down Expand Up @@ -814,13 +833,6 @@ def distributed_send_message(message):
def fetch_and_send_message():
message = send_queue.get()

if not quota.allow_send(message):
logger.warn('Hard message quota exceeded; Dropping this message on floor: %s', message)
message_id = message.get('message_id')
if message_id:
spawn(auditlog.message_change, message_id, auditlog.SENT_CHANGE, '', '', 'Dropping due to hard quota violation.')
return

has_contact = set_target_contact(message)
if not has_contact:
mark_message_has_no_contact(message)
Expand All @@ -829,6 +841,34 @@ def fetch_and_send_message():
if 'message_id' not in message:
message['message_id'] = None

# If this app breaches hard quota, drop message on floor, and update in UI if it has an ID
if not quota.allow_send(message):
logger.warn('Hard message quota exceeded; Dropping this message on floor: %s', message)
if message['message_id']:
drop_mode_id = api_cache.modes.get('drop')
spawn(auditlog.message_change, message['message_id'], auditlog.MODE_CHANGE, message.get('mode', '?'), 'drop', 'Dropping due to hard quota violation.')

# If we know the ID for the mode drop, reflect that for the message
if drop_mode_id:
message['mode'] = 'drop'
message['mode_id'] = drop_mode_id
else:
logger.error('Can\'t mark message %s as dropped as we don\'t know the mode ID for %s', message, 'drop')

# Render, so we're able to populate the message table with the proper subject/etc as well as
# information that it was dropped.
render(message)
mark_message_as_sent(message)
return

# If we're set to drop this message, no-op this before message gets sent to a vendor
if message.get('mode') == 'drop':
logging.info('Deliberately dropping message %s', message)
if message['message_id']:
render(message)
mark_message_as_sent(message)
return

render(message)
success = None
try:
Expand Down Expand Up @@ -910,6 +950,7 @@ def init_sender(config):
init_metrics(config, 'iris-sender', default_sender_metrics)
api_cache.cache_priorities()
api_cache.cache_applications()
api_cache.cache_modes()

global should_mock_gwatch_renewer, send_message
if config['sender'].get('debug'):
Expand Down
5 changes: 5 additions & 0 deletions src/iris_api/cache.py
Expand Up @@ -21,6 +21,11 @@ def cache_applications():
'SELECT `name` FROM `template_variable` WHERE `application_id` = %s',
app['id'])
app['variables'] = [row['name'] for row in cursor]
cursor.execute('''SELECT `mode`.`name`
FROM `mode`
JOIN `application_mode` on `mode`.`id` = `application_mode`.`mode_id`
WHERE `application_mode`.`application_id` = %s''', app['id'])
app['supported_modes'] = [row['name'] for row in cursor]
applications[app['name']] = app
connection.close()
cursor.close()
Expand Down
12 changes: 10 additions & 2 deletions src/iris_api/sender/quota.py
Expand Up @@ -24,8 +24,8 @@
`application_quota`.`wait_time`
FROM `application_quota`
JOIN `application` ON `application`.`id` = `application_quota`.`application_id`
JOIN `target` on `target`.`id` = `application_quota`.`target_id`
JOIN `target_type` on `target_type`.`id` = `target`.`type_id` '''
LEFT JOIN `target` on `target`.`id` = `application_quota`.`target_id`
LEFT JOIN `target_type` on `target_type`.`id` = `target`.`type_id` '''

create_incident_query = '''INSERT INTO `incident` (`plan_id`, `created`, `context`, `current_step`, `active`, `application_id`)
VALUES ((SELECT `plan_id` FROM `plan_active` WHERE `name` = :plan_name),
Expand Down Expand Up @@ -133,6 +133,10 @@ def notify_incident(self, application, limit, duration, plan_name, wait_time):
logger.warning('Application %s breached hard quota. Cannot notify owners as application is not set')
return

if not plan_name:
logger.error('Application %s breached hard quota. Cannot create iris incident as plan is not set (may have been deleted).')
return

logger.warning('Application %s breached hard quota. Will create incident using plan %s', application, plan_name)

session = self.db.Session()
Expand Down Expand Up @@ -181,6 +185,10 @@ def notify_target(self, application, limit, duration, target_name, target_role):
logger.warning('Application %s breached soft quota. Cannot notify owners as application is not set')
return

if not target_name or not target_role:
logger.error('Application %s breached soft quota. Cannot notify owner as they aren\'t set (may have been deleted).')
return

logger.warning('Application %s breached soft quota. Will notify %s:%s', application, target_role, target_name)

targets = self.expand_targets(target_role, target_name)
Expand Down
3 changes: 2 additions & 1 deletion test/e2etest.py
Expand Up @@ -1239,7 +1239,7 @@ def test_post_plan_noc(sample_user, sample_team, sample_application_name):


def test_get_applications(sample_application_name):
app_keys = set(['variables', 'required_variables', 'name', 'context_template', 'summary_template', 'sample_context', 'default_modes'])
app_keys = set(['variables', 'required_variables', 'name', 'context_template', 'summary_template', 'sample_context', 'default_modes', 'supported_modes'])
# TODO: insert application data before get
re = requests.get(base_url + 'applications/' + sample_application_name)
assert re.status_code == 200
Expand Down Expand Up @@ -1344,6 +1344,7 @@ def test_get_modes():
assert 'email' in data
assert 'call' in data
assert 'im' in data
assert 'drop' not in data


def test_get_priorities():
Expand Down
1 change: 1 addition & 0 deletions test/test_sender.py
Expand Up @@ -14,6 +14,7 @@ def test_configure(mocker):
mocker.patch('iris_api.db.init')
mocker.patch('iris_api.bin.sender.api_cache.cache_priorities')
mocker.patch('iris_api.bin.sender.api_cache.cache_applications')
mocker.patch('iris_api.bin.sender.api_cache.cache_modes')
init_sender({
'db': {
'conn': {
Expand Down