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

Implement multiple flagstores per service #87

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 101 additions & 29 deletions conf/controller/scoring.sql
Original file line number Diff line number Diff line change
Expand Up @@ -28,49 +28,121 @@ WITH
FROM flagdefense
GROUP BY team_id, service_id
),
sla_ok AS (
SELECT count(*) as sla_ok,
team_id,
service_id
FROM scoring_statuscheck
WHERE status = 0
GROUP BY team_id, service_id
),
sla_recover AS (
SELECT 0.5 * count(*) as sla_recover,
team_id,
service_id
FROM scoring_statuscheck
WHERE status = 4
GROUP BY team_id, service_id
),
-- sla_ok AS (
-- SELECT count(*) as sla_ok,
-- team_id,
-- service_id
-- FROM scoring_statuscheck
-- WHERE status = 0
-- GROUP BY team_id, service_id
-- ),
-- sla_recover AS (
-- SELECT 0.5 * count(*) as sla_recover,
-- team_id,
-- service_id
-- FROM scoring_statuscheck
-- WHERE status = 4
-- GROUP BY team_id, service_id
-- ),
teams as (
SELECT user_id as team_id
FROM registration_team
INNER JOIN auth_user ON auth_user.id = registration_team.user_id
WHERE is_active = true
AND nop_team = false
),
sla AS (
-- sla AS (
-- SELECT (SELECT sqrt(count(*)) FROM teams) * (coalesce(sla_ok, 0) + coalesce(sla_recover, 0)) as sla,
-- team_id,
-- service_id
-- FROM sla_ok
-- NATURAL FULL OUTER JOIN sla_recover
-- ),
fill AS (
SELECT team_id, scoring_servicegroup.id AS service_group_id
FROM teams, scoring_servicegroup
),
servicegroup AS (
SELECT scoring_service.service_group_id AS service_group_id,
count(scoring_service.id) AS services_count
FROM scoring_service
GROUP BY scoring_service.service_group_id
),
attack_by_servicegroup AS (
SELECT team_id,
scoring_service.service_group_id AS service_group_id,
sum(attack) AS attack,
sum(bonus) AS bonus
FROM attack
INNER JOIN scoring_service ON scoring_service.id = attack.service_id
GROUP BY team_id, scoring_service.service_group_id
),
defense_by_servicegroup AS (
SELECT team_id,
scoring_service.service_group_id AS service_group_id,
sum(defense) AS defense
FROM defense
INNER JOIN scoring_service ON scoring_service.id = defense.service_id
GROUP BY team_id, scoring_service.service_group_id
),
sla_ok_by_servicegroup_tick AS (
SELECT team_id,
scoring_service.service_group_id AS service_group_id,
0.5 * servicegroup.services_count * (COUNT(*) = servicegroup.services_count)::int AS sla_ok
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the SLA formula for each service group, which should be easy to replace in case we want to use another formula.

Copy link
Author

@FlorianKothmeier FlorianKothmeier Mar 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This actually evaluates to 0.5 * len(services[group]) for each check result instead of 0.5 * len(services[group])**2 as I initially thought.

But, because it gets evaluated once for each service in the group this still results in 0.5 * len(services[group])**2. I think this was unintended, as their scoring provides the formula

sla = (count(ticks_with_status['up'] + 0.5 * ticks_with_status['recovering'])) * sqrt(count(teams)) * count(flag_stores_in_service)

Also note that this formula is the same as the formula for recovering sla points, so this formula should have had a factor of 1.0 instead.

To get the documented behavior, the formulas should have been:
sla: 1.0 * (COUNT(*) = servicegroup.services_count)::int
sla_recover: 0.5 * (COUNT(*) = servicegroup.services_count)::int

EDIT: their formula does work as intended actually

Some additional thoughts:

  • Do we want sla to scale linearly with the number of flagstores? Other options include sla that doesn't depend on the number of flagstores or some non-linear scaling (maybe sqrt(len(services[group])?)
  • Currently no partial sla points are awarded for having some flagstores available, when others are down. Maybe we should award y/x * sla if y of x services in the group are up?

FROM scoring_statuscheck
INNER JOIN scoring_service ON scoring_service.id = scoring_statuscheck.service_id
INNER JOIN servicegroup ON servicegroup.service_group_id = scoring_service.service_group_id
WHERE scoring_statuscheck.status = 0
GROUP BY team_id, scoring_service.service_group_id, servicegroup.services_count, scoring_statuscheck.tick
),
sla_ok_by_servicegroup AS (
SELECT team_id,
service_group_id,
sum(sla_ok) AS sla_ok
FROM sla_ok_by_servicegroup_tick
GROUP BY team_id, service_group_id
),
sla_recover_by_servicegroup_tick AS (
SELECT team_id,
scoring_service.service_group_id AS service_group_id,
0.5 * servicegroup.services_count * (COUNT(*) = servicegroup.services_count)::int AS sla_recover
FROM scoring_statuscheck
INNER JOIN scoring_service ON scoring_service.id = scoring_statuscheck.service_id
INNER JOIN servicegroup ON servicegroup.service_group_id = scoring_service.service_group_id
WHERE scoring_statuscheck.status = 4 OR scoring_statuscheck.status = 0
GROUP BY team_id, scoring_service.service_group_id, servicegroup.services_count, scoring_statuscheck.tick
),
sla_recover_by_servicegroup AS (
SELECT team_id,
service_group_id,
sum(sla_recover) AS sla_recover
FROM sla_recover_by_servicegroup_tick
GROUP BY team_id, service_group_id
),
sla_by_servicegroup AS (
SELECT (SELECT sqrt(count(*)) FROM teams) * (coalesce(sla_ok, 0) + coalesce(sla_recover, 0)) as sla,
team_id,
service_id
FROM sla_ok
NATURAL FULL OUTER JOIN sla_recover
),
fill AS (
SELECT team_id, scoring_service.id AS service_id
FROM teams, scoring_service
service_group_id
FROM sla_ok_by_servicegroup
NATURAL FULL OUTER JOIN sla_recover_by_servicegroup
-- ),
-- sla_by_servicegroup AS ( -- alternative sla without interplay between servicegroups
-- SELECT team_id,
-- scoring_service.service_group_id AS service_group_id,
-- sum(sla) AS sla
-- FROM sla
-- INNER JOIN scoring_service ON scoring_service.id = sla.service_id
-- GROUP BY team_id, scoring_service.service_group_id
)
SELECT team_id,
service_id,
service_group_id,
(coalesce(attack, 0)+coalesce(bonus, 0))::double precision as attack,
coalesce(bonus, 0) as bonus,
coalesce(defense, 0)::double precision as defense,
coalesce(sla, 0) as sla,
coalesce(attack, 0) + coalesce(defense, 0) + coalesce(bonus, 0) + coalesce(sla, 0) as total
FROM attack
NATURAL FULL OUTER JOIN defense
NATURAL FULL OUTER JOIN sla
FROM attack_by_servicegroup
NATURAL FULL OUTER JOIN defense_by_servicegroup
NATURAL FULL OUTER JOIN sla_by_servicegroup
NATURAL INNER JOIN fill
ORDER BY team_id, service_id;
ORDER BY team_id, service_group_id;
16 changes: 8 additions & 8 deletions examples/checker/example_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ def place_flag(self, tick):
logging.info('Received response to SET command: %s', repr(resp))
except UnicodeDecodeError:
logging.warning('Received non-UTF-8 data: %s', repr(resp))
return checkerlib.CheckResult.FAULTY
return checkerlib.CheckResult.FAULTY, 'Received non-UTF-8 data'
if resp != 'OK':
logging.warning('Received wrong response to SET command')
return checkerlib.CheckResult.FAULTY
return checkerlib.CheckResult.FAULTY, 'Received wrong response to SET command'

conn.close()
return checkerlib.CheckResult.OK
return checkerlib.CheckResult.OK, ''

def check_service(self):
conn = connect(self.ip)
Expand All @@ -37,10 +37,10 @@ def check_service(self):
logging.info('Received response to dummy command')
except UnicodeDecodeError:
logging.warning('Received non-UTF-8 data')
return checkerlib.CheckResult.FAULTY
return checkerlib.CheckResult.FAULTY, 'Received non-UTF-8 data'

conn.close()
return checkerlib.CheckResult.OK
return checkerlib.CheckResult.OK, ''

def check_flag(self, tick):
flag = checkerlib.get_flag(tick)
Expand All @@ -54,13 +54,13 @@ def check_flag(self, tick):
logging.info('Received response to GET command: %s', repr(resp))
except UnicodeDecodeError:
logging.warning('Received non-UTF-8 data: %s', repr(resp))
return checkerlib.CheckResult.FAULTY
return checkerlib.CheckResult.FAULTY, 'Received non-UTF-8 data'
if resp != flag:
logging.warning('Received wrong response to GET command')
return checkerlib.CheckResult.FLAG_NOT_FOUND
return checkerlib.CheckResult.FLAG_NOT_FOUND, 'Received wrong response to GET command'

conn.close()
return checkerlib.CheckResult.OK
return checkerlib.CheckResult.OK, ''


def connect(ip):
Expand Down
12 changes: 8 additions & 4 deletions src/ctf_gameserver/checker/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,11 @@ def get_check_duration(db_conn, service_id, std_dev_count, prohibit_changes=Fals
' WHERE service_id = %s AND tick < current_tick', (std_dev_count, service_id))
result = cursor.fetchone()

return result[0]
if result and len(result) == 1 and result[0] != None:
return float(result[0])
else:
logging.warning('could not calculate the average script runtime in get_check_duration')
return 20


def get_task_count(db_conn, service_id, prohibit_changes=False):
Expand Down Expand Up @@ -152,7 +156,7 @@ def _net_no_to_team_id(cursor, team_net_no, fake_team_id):
return data[0]


def commit_result(db_conn, service_id, team_net_no, tick, result, prohibit_changes=False, fake_team_id=None):
def commit_result(db_conn, service_id, team_net_no, tick, result, message='', prohibit_changes=False, fake_team_id=None):
"""
Saves the result from a Checker run to game database.
"""
Expand All @@ -164,8 +168,8 @@ def commit_result(db_conn, service_id, team_net_no, tick, result, prohibit_chang
return

cursor.execute('INSERT INTO scoring_statuscheck'
' (service_id, team_id, tick, status, timestamp)'
' VALUES (%s, %s, %s, %s, NOW())', (service_id, team_id, tick, result))
' (service_id, team_id, tick, status, timestamp, message)'
' VALUES (%s, %s, %s, %s, NOW(), %s)', (service_id, team_id, tick, result, message))
# (In case of `prohibit_changes`,) PostgreSQL checks the database grants even if nothing is matched
# by `WHERE`
cursor.execute('UPDATE scoring_flag'
Expand Down
20 changes: 14 additions & 6 deletions src/ctf_gameserver/checker/master.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,24 +311,32 @@ def handle_store_request(self, task_info, params):

def handle_result_request(self, task_info, param):
try:
result = int(param)
except ValueError:
result = int(param['value'])
except ValueError | KeyError:
logging.error('Invalid result from Checker Script for team %d (net number %d) in tick %d: %s',
task_info['_team_id'], task_info['team'], task_info['tick'], param)
return

try:
check_result = CheckResult(result)
check_result = CheckResult(param['value'])
except ValueError:
logging.error('Invalid result from Checker Script for team %d (net number %d) in tick %d: %d',
task_info['_team_id'], task_info['team'], task_info['tick'], result)
return

logging.info('Result from Checker Script for team %d (net number %d) in tick %d: %s',
task_info['_team_id'], task_info['team'], task_info['tick'], check_result)
try:
message = str(param['message'])
except ValueError | KeyError:
logging.error('Invalid result from Checker Script for team %d (net number %d) in tick %d: %s',
task_info['_team_id'], task_info['team'], task_info['tick'], param)
return


logging.info('Result from Checker Script for team %d (net number %d) in tick %d: %s, msg: %s',
task_info['_team_id'], task_info['team'], task_info['tick'], check_result, message)
metrics.inc(self.metrics_queue, 'completed_tasks', labels={'result': check_result.name})
database.commit_result(self.db_conn, self.service['id'], task_info['team'], task_info['tick'],
result)
result, message)

def launch_tasks(self):
def change_tick(new_tick):
Expand Down
9 changes: 4 additions & 5 deletions src/ctf_gameserver/checker/supervisor.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,13 +361,12 @@ def handle_script_message(message, ctrlin_fd, runner_id, queue_to_master, pipe_f

if action == ACTION_RESULT:
try:
result = CheckResult(int(param))
except ValueError:
result = CheckResult(int(param['value']))
script_logger.info('[RUNNER] Checker Script result: %s, msg: %s', result.name, param['message'],
extra={'result': result.value})
except ValueError | KeyError:
# Ignore malformed message from the Checker Script, will be logged by the Master
pass
else:
script_logger.info('[RUNNER] Checker Script result: %s', result.name,
extra={'result': result.value})

queue_to_master.put((runner_id, action, param))
response = pipe_from_master.recv()
Expand Down
2 changes: 1 addition & 1 deletion src/ctf_gameserver/checkerlib/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from .lib import BaseChecker, CheckResult, get_flag, set_flagid, load_state, run_check, store_state
from .lib import BaseChecker, CheckResult, get_flag, set_flagid, get_flagid, load_state, run_check, store_state
Loading
Loading