split-silo-database 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. #!/usr/bin/env python
  2. import os
  3. import click
  4. from django.apps import apps
  5. from sentry.runner import configure
  6. from sentry.runner.commands.devservices import get_docker_client
  7. from sentry.silo.base import SiloMode
  8. configure()
  9. from django.conf import settings
  10. from sentry.models.organizationmapping import OrganizationMapping
  11. def exec_run(container, command):
  12. wrapped_command = f'sh -c "{" ".join(command)}"'
  13. exit_code, output = container.exec_run(cmd=wrapped_command, stdout=True, stderr=True)
  14. if exit_code:
  15. click.echo("Container operation Failed!")
  16. click.echo(f"Container operation failed with {output}")
  17. return output
  18. def split_database(tables: list[str], source: str, destination: str, reset: bool, verbose: bool):
  19. click.echo(f">> Dumping tables from {source} database")
  20. command = ["pg_dump", "-U", "postgres", "-d", source, "--clean"]
  21. for table in tables:
  22. command.extend(["-t", table])
  23. command.extend([">", f"/tmp/{destination}-tables.sql"])
  24. with get_docker_client() as client:
  25. postgres_container = (
  26. "sentry_postgres"
  27. if os.environ.get("USE_NEW_DEVSERVICES") != "1"
  28. else "sentry-postgres-1"
  29. )
  30. postgres = client.containers.get(postgres_container)
  31. if verbose:
  32. click.echo(f">> Running {' '.join(command)}")
  33. exec_run(postgres, command)
  34. if reset:
  35. click.echo(f">> Dropping existing {destination} database")
  36. exec_run(postgres, ["dropdb", "-U", "postgres", "--if-exists", destination])
  37. exec_run(postgres, ["createdb", "-U", "postgres", destination])
  38. citext_command = [
  39. "psql",
  40. "-U",
  41. "postgres",
  42. destination,
  43. "-c",
  44. "'CREATE EXTENSION IF NOT EXISTS citext'",
  45. ]
  46. if verbose:
  47. click.echo(f">> RUNNING: {' '.join(citext_command)}")
  48. exec_run(postgres, citext_command)
  49. # Use the dump file to build control silo tables.
  50. click.echo(f">> Building {destination} database from dump file")
  51. import_command = [
  52. "psql",
  53. "-U",
  54. "postgres",
  55. destination,
  56. "<",
  57. f"/tmp/{destination}-tables.sql",
  58. ]
  59. if verbose:
  60. click.echo(f">> Running {' '.join(import_command)}")
  61. exec_run(postgres, import_command)
  62. if destination == "region" and reset:
  63. click.echo(">> Cloning stored procedures")
  64. function_dump = [
  65. "psql",
  66. "-U",
  67. "postgres",
  68. source,
  69. "-c",
  70. "'\\sf sentry_increment_project_counter'",
  71. ]
  72. function_sql = exec_run(postgres, function_dump)
  73. import_function = [
  74. "psql",
  75. "-U",
  76. "postgres",
  77. destination,
  78. "-c",
  79. "'" + function_sql.decode("utf8") + "'",
  80. ]
  81. exec_run(postgres, import_function)
  82. def revise_organization_mappings(legacy_region_name: str):
  83. if settings.SENTRY_MONOLITH_REGION == legacy_region_name:
  84. click.echo(
  85. "> No OrganizationMapping have been modified. Set 'SENTRY_MONOLITH_REGION' in sentry.conf.py to update monolith mappings."
  86. )
  87. else:
  88. qs = OrganizationMapping.objects.filter(region_name=legacy_region_name)
  89. record_count = len(qs)
  90. qs.update(region_name=settings.SENTRY_MONOLITH_REGION)
  91. click.echo(
  92. f"> {record_count} OrganizationMapping record(s) have been updated from '{legacy_region_name}' to '{settings.SENTRY_MONOLITH_REGION}'"
  93. )
  94. @click.command()
  95. @click.option(
  96. "--legacy-region-name",
  97. default="--monolith--",
  98. help="Previous value of settings.SENTRY_MONOLITH_REGION to overwrite in organization mappings",
  99. )
  100. @click.option("--verbose", default=False, is_flag=True, help="Enable verbose logging")
  101. @click.option(
  102. "--reset",
  103. default=False,
  104. is_flag=True,
  105. help="Reset the target databases to be empty before loading extracted data and schema.",
  106. )
  107. @click.option("--database", default="sentry", help="Which database to derive splits from")
  108. def main(database: str, reset: bool, verbose: bool, legacy_region_name: str):
  109. """
  110. This is a development tool that can convert a monolith database into
  111. control + region databases by using silo annotations.
  112. This operation will not modify the original source database.
  113. """
  114. # We have a few tables that either need to be in both silos,
  115. # or only in control. These tables don't have silo annotations
  116. # as they are inherited from django and their silo assignments
  117. # need to be manually defined.
  118. region_tables = ["django_migrations", "django_content_type"]
  119. control_tables = [
  120. "django_migrations",
  121. "django_admin_log",
  122. "django_content_type",
  123. "django_site",
  124. "django_session",
  125. "auth_user",
  126. "auth_group",
  127. "auth_permission",
  128. "auth_group_permissions",
  129. "auth_user_groups",
  130. "auth_user_user_permissions",
  131. ]
  132. for model in apps.get_models():
  133. silo_limit = getattr(model._meta, "silo_limit", None)
  134. if not silo_limit:
  135. click.echo(f"> Could not find silo assignment for {model._meta.db_table}")
  136. continue
  137. if SiloMode.CONTROL in silo_limit.modes:
  138. control_tables.append(model._meta.db_table)
  139. if SiloMode.REGION in silo_limit.modes:
  140. region_tables.append(model._meta.db_table)
  141. revise_organization_mappings(legacy_region_name=legacy_region_name)
  142. split_database(control_tables, database, "control", reset=reset, verbose=verbose)
  143. split_database(region_tables, database, "region", reset=reset, verbose=verbose)
  144. if __name__ == "__main__":
  145. main()