# ============================================================
# This file attempts to obtain a global lock (for a given
# region / account combination.
#
# This makes one attempt to get the lock and will set the
# won_lock variable to True or False to indicate whether
# or not we got the lock.
#
# It's expected that this will be executed in a retry loop
# so that if we don't get the lock we delay then try again.
#
# This should only be used in a block with cleanup-lock.yaml
# included in the always clause to ensure the lock is released.
#
# There are several variables that control the locking behaviour:
# * lock_timeout_seconds
#     How old a lock must be before it's assumed to be an expired
#     lock that was not cleaned up by the owner. Any locks older
#     than this will not prevent a lock being obtained and will
#     be deleted when a new process obtains the lock.
# * lock_log_group_prefix
#     The log_group prefix that represents the lock being obtained.
#     This must be the same across all processes trying to obtain
#     the lock.
# * lock_process_id
#     A unique identifier of this process. Each process that might
#     attempt to lock the process must have a different identifier.
#     This defaults to the resource_prefix which is generally
#     appropriate.
# * max_obtain_lock_attempts
#     How many attempts to make to get the lock before giving up
#     NB: This is actually done in main.yaml
# * obtain_lock_delay_seconds:
#     How long to delay after failing to get the lock before
#     trying again.
#     NB: This is actually done in obtain-lock-wrapper.yaml
#
# The locking here is based around creating cloudwatch log groups.
# This resource was chosen because:
#   A) it's free
#   B) we have a built in grouping concept because of the hierarchy
#      that allows us to easily group attempts for the same lock
#   C) the creation time is tracked and returned which gives us
#      a mechanism for deterministically picking a winner
#
# Each lock is represented by a log group prefix. Each attempt
# to obtain the lock is a log group of the lock_process_id below
# that prefix.
#
# The winning lock is the one with the earliest creation time.
#
# To prevent a hanging lock from permanently hanging the build
# lock attempts older than the lock timeout are ignored and
# cleaned up by the next process to win the lock.
# ============================================================

- name: set up aws connection info
  set_fact:
    aws_connection_info: &aws_connection_info
      aws_access_key: "{{ aws_access_key }}"
      aws_secret_key: "{{ aws_secret_key }}"
      security_token: "{{ security_token }}"
      region: "{{ aws_region }}"
  no_log: yes

- name: Set lock_attempt_log_group_name
  set_fact:
    lock_attempt_log_group_name: "{{ lock_log_group_prefix }}/{{ lock_process_id|default(resource_prefix) }}"

  # Note the overwrite below to ensure that the creation time
  # is upated. This is important as we calculate expiry relative
  # the attempt creation.
  #
  # Because of this it's imporatnt that we delete the attempt
  # if we don't get the lock. Otherwise we can get a deadlock
  # where the stale atttempt from one process wins, but then
  # because that process updates the creation date it doesn't
  # consider its self to havewone.
- name: Create Lock Attempt Log Group
  cloudwatchlogs_log_group:
    log_group_name: "{{ lock_attempt_log_group_name }}"
    state: present
    overwrite: True
    <<: *aws_connection_info
  register: lock_attempt_log_group_result

- name: Get Lock Attempt Lock Groups
  cloudwatchlogs_log_group_info:
    log_group_name: "{{ lock_log_group_prefix }}/"
    <<: *aws_connection_info
  register: lock_attempt_log_groups

- name: Calculate Expired Lock Attempt Timestamp
  set_fact:
    expired_lock_timestamp: "{{ lock_attempt_log_group_result.creation_time - (lock_timeout_seconds * 1000) }}"

- name: Get Expired and Active Lock Attempts
  set_fact:
    expired_lock_attempts: "{{ lock_attempt_log_groups.log_groups|selectattr('creation_time', 'lt', expired_lock_timestamp|int)|list }}"
    active_lock_attempts: "{{ lock_attempt_log_groups.log_groups|selectattr('creation_time', 'ge', expired_lock_timestamp|int)|list }}"

- name: Pick Winning Lock Attempt
  set_fact:
    winning_lock_attempt: "{{ active_lock_attempts|sort(attribute='creation_time')|first }}"

- name: Determine if Won Lock
  set_fact:
    won_lock: "{{ winning_lock_attempt.log_group_name == lock_attempt_log_group_name }}"

  # Remove the lock attempt if we didn't get the lock. This prevents
  # our stale lock attempt blocking another process from getting the lock.
  # See more detailed comment above Create Lock Attempt Log Group
- name: Remove Failed Lock Attempt Log Group
  cloudwatchlogs_log_group:
    log_group_name: "{{ lock_attempt_log_group_name }}"
    state: absent
    <<: *aws_connection_info
  when: "not won_lock|bool"

- name: Delete Expired Lock Attempts
  cloudwatchlogs_log_group:
    log_group_name: "{{ item.log_group_name }}"
    state: absent
    <<: *aws_connection_info
  when: "won_lock|bool"
  loop: "{{ expired_lock_attempts }}"
