panda_pal 5.11.0 → 5.12.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +12 -0
- data/app/models/panda_pal/organization.rb +3 -39
- data/app/models/panda_pal/organization_concerns/multi_database_sharding.rb +22 -0
- data/app/models/panda_pal/organization_concerns/tenant_handling.rb +54 -0
- data/config/initializers/apartment.rb +202 -3
- data/lib/panda_pal/engine.rb +0 -2
- data/lib/panda_pal/spec_helper.rb +85 -0
- data/lib/panda_pal/version.rb +1 -1
- data/panda_pal.gemspec +3 -3
- data/spec/core/apartment_multidb_spec.rb +48 -0
- data/spec/dummy/config/database.yml +6 -2
- data/spec/dummy/config/environments/test.rb +5 -0
- data/spec/dummy/db/test2_schema.rb +18 -0
- data/spec/models/panda_pal/api_call_spec.rb +1 -1
- data/spec/models/panda_pal/organization/settings_validation_spec.rb +4 -0
- data/spec/models/panda_pal/organization/task_scheduling_spec.rb +0 -0
- data/spec/rails_helper.rb +4 -0
- data/spec/spec_helper.rb +0 -1
- metadata +52 -45
- /data/spec/dummy/{bin → app/bin}/bundle +0 -0
- /data/spec/dummy/{bin → app/bin}/rails +0 -0
- /data/spec/dummy/{bin → app/bin}/rake +0 -0
- /data/spec/dummy/{bin → app/bin}/setup +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c7b257eddedb1431e4a8030cae79245ef3c0da635ac47cd7e3d1b7ac223d630c
|
4
|
+
data.tar.gz: cff44cd6c69723032155a0e82a952f1ceaec1bbcc991014ab5ea5fd75a3404db
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 398fedeae33152e8691af37e3c32d3fc353e379839e42837779652e48d8ec2e22112eb6da0b0e24f942aaf84a727102e67d85ee573b12b5ccd42595675413a97
|
7
|
+
data.tar.gz: cf6e76fa5dc3aa9c338d8ba118c2f4107f945eebb4e5ebeaf059db4e2119abc69879ee38467452f0d6852855a72606e148c6ba0d05da4c60001cf3145376ba9f
|
data/README.md
CHANGED
@@ -179,6 +179,18 @@ Rails.application.config.middleware.use 'Apartment::Elevators::Generic', lambda
|
|
179
179
|
It is also possible to switch tenants by implementing `around_action :switch_tenant` in the controller.
|
180
180
|
However, it should be noted that calling `switch_tenant` must be used in an `around_action` hook (or given a block), and is only intended to be used for LTI launches, and not subsequent requests.
|
181
181
|
|
182
|
+
### Sharding
|
183
|
+
PandaPal 5.12 added support for multi-DB sharding. If you wish to use this, add a `shard` column to the `PandaPal::Organization` model. The implementation should be fairly transparent and should behave as normal PandaPal/Apartment - the only difference is that tenants can be created on different DB shards.
|
184
|
+
|
185
|
+
The list of available shards is computed from the Environment variables. On startup, the application looks for variables with the following formats:
|
186
|
+
- `SHARD_DB_XYZ_URL`
|
187
|
+
- `HEROKU_POSTGRESQL_XYZ_URL`
|
188
|
+
(where `XYZ` is the shard identifier)
|
189
|
+
|
190
|
+
When an `Organization` is created, it is assigned to a random shard (or if no shards were discovered, it is placed in the default database as normal). Alternatively, a specific shard can be specified: `PandaPal::Organization.new(shard: "shard_a")`. An `Organization` can be added to the primary shard with `shard: "default"`
|
191
|
+
|
192
|
+
The `PandaPal::Organization` record is still created in the `public` schema of the default database, as per usual.
|
193
|
+
|
182
194
|
### Rake tasks and jobs
|
183
195
|
**Delayed Job Support has been removed. This allows each project to assess it's need to handle background jobs.**
|
184
196
|
|
@@ -41,6 +41,9 @@ module PandaPal
|
|
41
41
|
include OrganizationConcerns::OrganizationBuilder if $stdin&.tty?
|
42
42
|
include OrganizationConcerns::TaskScheduling if defined?(Sidekiq.schedule)
|
43
43
|
|
44
|
+
include OrganizationConcerns::TenantHandling
|
45
|
+
include OrganizationConcerns::MultiDatabaseSharding if (column_names rescue []).include?('shard')
|
46
|
+
|
44
47
|
attr_encrypted :settings, marshal: true, key: :encryption_key, marshaler: SettingsMarshaler
|
45
48
|
before_save {|a| a.settings = a.settings} # this is a hacky work-around to a bug where attr_encrypted is not saving settings in place
|
46
49
|
|
@@ -56,9 +59,6 @@ module PandaPal
|
|
56
59
|
validates :canvas_account_id, presence: true
|
57
60
|
validates :salesforce_id, presence: true, uniqueness: true
|
58
61
|
|
59
|
-
after_create :create_schema
|
60
|
-
after_commit :destroy_schema, on: :destroy
|
61
|
-
|
62
62
|
define_setting("lti", {
|
63
63
|
type: 'Hash',
|
64
64
|
required: false,
|
@@ -71,10 +71,6 @@ module PandaPal
|
|
71
71
|
end
|
72
72
|
end
|
73
73
|
|
74
|
-
before_validation on: [:update] do
|
75
|
-
errors.add(:name, 'should not be changed after creation') if name_changed?
|
76
|
-
end
|
77
|
-
|
78
74
|
def encryption_key
|
79
75
|
# production environment might not have loaded secret_key_base yet.
|
80
76
|
# In that case, just read it from env.
|
@@ -85,18 +81,6 @@ module PandaPal
|
|
85
81
|
end
|
86
82
|
end
|
87
83
|
|
88
|
-
def switch_tenant(&block)
|
89
|
-
if block_given?
|
90
|
-
Apartment::Tenant.switch(name, &block)
|
91
|
-
else
|
92
|
-
Apartment::Tenant.switch!(name)
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
def self.current
|
97
|
-
find_by_name(Apartment::Tenant.current)
|
98
|
-
end
|
99
|
-
|
100
84
|
def create_api(logic, expiration: nil, uses: nil, host: nil)
|
101
85
|
switch_tenant do
|
102
86
|
logic = "current_organization.#{logic}" if logic.is_a?(Symbol)
|
@@ -109,16 +93,6 @@ module PandaPal
|
|
109
93
|
end
|
110
94
|
end
|
111
95
|
|
112
|
-
def rename!(new_name)
|
113
|
-
do_switch = Apartment::Tenant.current == name
|
114
|
-
ActiveRecord::Base.connection.execute(
|
115
|
-
"ALTER SCHEMA \"#{name}\" RENAME TO \"#{new_name}\";"
|
116
|
-
)
|
117
|
-
self.class.where(id: id).update_all(name: new_name)
|
118
|
-
reload
|
119
|
-
switch_tenant if do_switch
|
120
|
-
end
|
121
|
-
|
122
96
|
if !PandaPal.lti_options[:platform].present? || PandaPal.lti_options[:platform].is_a?(String)
|
123
97
|
CONST_PLATFORM_TYPE = Platform.resolve_platform_class(nil) rescue nil
|
124
98
|
else
|
@@ -192,16 +166,6 @@ module PandaPal
|
|
192
166
|
CONST_PLATFORM_TYPE
|
193
167
|
end
|
194
168
|
|
195
|
-
private
|
196
|
-
|
197
|
-
def create_schema
|
198
|
-
Apartment::Tenant.create name
|
199
|
-
end
|
200
|
-
|
201
|
-
def destroy_schema
|
202
|
-
Apartment::Tenant.drop name
|
203
|
-
end
|
204
|
-
|
205
169
|
public
|
206
170
|
|
207
171
|
PandaPal.resolved_extensions_for(self).each do |ext|
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module PandaPal
|
2
|
+
module OrganizationConcerns
|
3
|
+
module MultiDatabaseSharding
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
if column_names.include?('shard')
|
8
|
+
validates :shard, format: { with: /\A[a-z0-9_]+\z/i }, allow_blank: true
|
9
|
+
|
10
|
+
before_validation on: [:update] do
|
11
|
+
errors.add(:shard, 'should not be changed after creation') if shard_changed?
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def tenant_name
|
17
|
+
return "#{shard}:#{name}" if shard.present?
|
18
|
+
super
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module PandaPal
|
2
|
+
module OrganizationConcerns
|
3
|
+
module TenantHandling
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
class_methods do
|
7
|
+
def current
|
8
|
+
find_by_name(Apartment::Tenant.current)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
included do
|
13
|
+
after_create :create_schema
|
14
|
+
after_commit :destroy_schema, on: :destroy
|
15
|
+
|
16
|
+
before_validation on: [:update] do
|
17
|
+
errors.add(:name, 'should not be changed after creation') if name_changed?
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def tenant_name
|
22
|
+
name
|
23
|
+
end
|
24
|
+
|
25
|
+
def switch_tenant(&block)
|
26
|
+
if block_given?
|
27
|
+
Apartment::Tenant.switch(tenant_name, &block)
|
28
|
+
else
|
29
|
+
Apartment::Tenant.switch!(tenant_name)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def rename!(new_name)
|
34
|
+
do_switch = Apartment::Tenant.current == name
|
35
|
+
ActiveRecord::Base.connection.execute(
|
36
|
+
"ALTER SCHEMA \"#{name}\" RENAME TO \"#{new_name}\";"
|
37
|
+
)
|
38
|
+
self.class.where(id: id).update_all(name: new_name)
|
39
|
+
reload
|
40
|
+
switch_tenant if do_switch
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def create_schema
|
46
|
+
Apartment::Tenant.create tenant_name
|
47
|
+
end
|
48
|
+
|
49
|
+
def destroy_schema
|
50
|
+
Apartment::Tenant.drop tenant_name
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -5,13 +5,212 @@ begin
|
|
5
5
|
rescue LoadError
|
6
6
|
end
|
7
7
|
|
8
|
+
require "apartment/adapters/postgresql_adapter"
|
9
|
+
|
10
|
+
module Apartment
|
11
|
+
SHARD_PREFIXES = ["SHARD_DB", "HEROKU_POSTGRESQL"]
|
12
|
+
|
13
|
+
def self.shard_configurations
|
14
|
+
$shard_configurations ||= begin
|
15
|
+
shard_to_env = {}
|
16
|
+
|
17
|
+
ENV.keys.each do |k|
|
18
|
+
m = /^(#{SHARD_PREFIXES.join("|")})_(\w+)_URL$/.match(k)
|
19
|
+
next unless m
|
20
|
+
|
21
|
+
url = ENV[k]
|
22
|
+
shard_to_env[m[2].downcase] = ActiveRecord::Base.configurations.resolve(url).configuration_hash
|
23
|
+
end
|
24
|
+
|
25
|
+
shard_to_env.freeze unless Rails.env.test?
|
26
|
+
|
27
|
+
shard_to_env
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module Tenant
|
32
|
+
self.singleton_class.send(:alias_method, :original_postgresql_adapter, :postgresql_adapter)
|
33
|
+
|
34
|
+
def self.postgresql_adapter(config)
|
35
|
+
if Apartment.with_multi_server_setup
|
36
|
+
adapter = Adapters::PostgresMultiDBSchemaAdapter
|
37
|
+
adapter.new(config)
|
38
|
+
else
|
39
|
+
original_postgresql_adapter(config)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.split_tenant(tenant)
|
44
|
+
bits = tenant.split(":", 2)
|
45
|
+
bits.unshift(nil) if bits.length == 1
|
46
|
+
bits[0] = "default" if bits[0].nil? || bits[0].empty?
|
47
|
+
bits
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
module Adapters
|
52
|
+
class PostgresMultiDBSchemaAdapter < Apartment::Adapters::PostgresqlSchemaAdapter
|
53
|
+
def initialize(*args, **kwargs)
|
54
|
+
super
|
55
|
+
@excluded_model_set = Set.new(Apartment.excluded_models)
|
56
|
+
end
|
57
|
+
|
58
|
+
def process_excluded_model(excluded_model)
|
59
|
+
@excluded_model_set << excluded_model
|
60
|
+
super
|
61
|
+
end
|
62
|
+
|
63
|
+
def is_excluded_model?(model)
|
64
|
+
@excluded_model_set.include?(model.to_s)
|
65
|
+
end
|
66
|
+
|
67
|
+
def db_connection_config(tenant)
|
68
|
+
shard, schema = Tenant.split_tenant(tenant)
|
69
|
+
if shard == "default"
|
70
|
+
@config
|
71
|
+
else
|
72
|
+
Apartment.shard_configurations[shard.downcase]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def drop_command(conn, tenant)
|
77
|
+
shard, schema = Tenant.split_tenant(tenant)
|
78
|
+
conn.execute(%(DROP SCHEMA "#{schema}" CASCADE))
|
79
|
+
end
|
80
|
+
|
81
|
+
def tenant_exists?(tenant)
|
82
|
+
return true unless Apartment.tenant_presence_check
|
83
|
+
shard, schema = Tenant.split_tenant(tenant)
|
84
|
+
|
85
|
+
Apartment.connection.schema_exists?(schema)
|
86
|
+
end
|
87
|
+
|
88
|
+
def create_tenant_command(conn, tenant)
|
89
|
+
shard, schema = Tenant.split_tenant(tenant)
|
90
|
+
# NOTE: This was causing some tests to fail because of the database strategy for rspec
|
91
|
+
if conn.open_transactions.positive?
|
92
|
+
conn.execute(%(CREATE SCHEMA "#{schema}")).inspect
|
93
|
+
else
|
94
|
+
schema = %(BEGIN;
|
95
|
+
CREATE SCHEMA "#{schema}";
|
96
|
+
COMMIT;)
|
97
|
+
|
98
|
+
conn.execute(schema)
|
99
|
+
end
|
100
|
+
rescue *rescuable_exceptions => e
|
101
|
+
rollback_transaction(conn)
|
102
|
+
raise e
|
103
|
+
end
|
104
|
+
|
105
|
+
def connect_to_new(tenant = nil)
|
106
|
+
return reset if tenant.nil?
|
107
|
+
|
108
|
+
current_tenant = @current
|
109
|
+
tenants_array = tenant.is_a?(Array) ? tenant.map(&:to_s) : tenant.to_s
|
110
|
+
tenant_schemas = map_to_schemas(Array(tenants_array))
|
111
|
+
query_cache_enabled = ActiveRecord::Base.connection.query_cache_enabled
|
112
|
+
|
113
|
+
@current = tenants_array
|
114
|
+
|
115
|
+
raise ActiveRecord::StatementInvalid, "PandaPal/Apartment Mutli-DB support does not currently support DB roles" if ActiveRecord::Base.current_role != ActiveRecord::Base.default_role
|
116
|
+
|
117
|
+
unless ActiveRecord::Base.connected?
|
118
|
+
Apartment.establish_connection multi_tenantify(tenant, false)
|
119
|
+
Apartment.connection.verify!
|
120
|
+
end
|
121
|
+
|
122
|
+
Apartment.connection.enable_query_cache! if query_cache_enabled
|
123
|
+
|
124
|
+
raise ActiveRecord::StatementInvalid, "Could not find schema for tenant #{tenant} (#{tenant_schemas.inspect})" unless schema_exists?(tenant_schemas)
|
125
|
+
|
126
|
+
Apartment.connection.schema_search_path = full_search_path
|
127
|
+
rescue *rescuable_exceptions => e
|
128
|
+
@current = current_tenant
|
129
|
+
raise_schema_connect_to_new(tenant, e)
|
130
|
+
end
|
131
|
+
|
132
|
+
protected
|
133
|
+
|
134
|
+
def persistent_schemas
|
135
|
+
map_to_schemas(super)
|
136
|
+
end
|
137
|
+
|
138
|
+
def map_to_schemas(tenants)
|
139
|
+
all_shard = nil
|
140
|
+
tenants.map do |schema|
|
141
|
+
shard, schema = Tenant.split_tenant(schema)
|
142
|
+
all_shard ||= shard
|
143
|
+
raise "Cannot mix shards in persistent_schemas" if shard != all_shard
|
144
|
+
schema
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
module CorePatches
|
151
|
+
module ActiveRecord
|
152
|
+
module FutureResult
|
153
|
+
# called in the original thread that knows the tenant
|
154
|
+
def initialize(_pool, *_args, **_kwargs)
|
155
|
+
@tenant = Apartment::Tenant.current
|
156
|
+
super
|
157
|
+
end
|
158
|
+
|
159
|
+
# called in the new thread with a connection that needs switching
|
160
|
+
def exec_query(_conn, *_args, **_kwargs)
|
161
|
+
Apartment::Tenant.switch!(@tenant) unless Apartment::Tenant.current == @tenant
|
162
|
+
super
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
module Base
|
167
|
+
extend ActiveSupport::Concern
|
168
|
+
|
169
|
+
included do
|
170
|
+
self.singleton_class.send(:alias_method, :pre_apartment_current_shard, :current_shard)
|
171
|
+
|
172
|
+
def self.current_shard
|
173
|
+
# This implementation is definitely a hack, but it should be fairly compatible. If you need to leverage
|
174
|
+
# Rails' sharding natively - you should just need to add models to the excluded_models list in Apartment
|
175
|
+
# to effectively disable this patch for that model.
|
176
|
+
if (adapter = Thread.current[:apartment_adapter]) && adapter.is_a?(Apartment::Adapters::PostgresMultiDBSchemaAdapter) && !adapter.is_excluded_model?(self)
|
177
|
+
shard, schema = Apartment::Tenant.split_tenant(adapter.current)
|
178
|
+
return "apt:#{shard}" unless shard == "default"
|
179
|
+
end
|
180
|
+
|
181
|
+
pre_apartment_current_shard
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# Fix Apartment's lack of support for load_async
|
189
|
+
::ActiveRecord::FutureResult.prepend CorePatches::ActiveRecord::FutureResult
|
190
|
+
|
191
|
+
# Hack ActiveRecord shards/connection_pooling to support our multi-DB approach
|
192
|
+
::ActiveRecord::Base.include CorePatches::ActiveRecord::Base
|
193
|
+
end
|
194
|
+
|
8
195
|
Apartment.configure do |config|
|
9
196
|
config.excluded_models ||= []
|
10
197
|
config.excluded_models |= ['PandaPal::Organization', 'PandaPal::Session']
|
11
198
|
|
12
|
-
config.
|
13
|
-
|
14
|
-
|
199
|
+
config.with_multi_server_setup = true unless Rails.env.test?
|
200
|
+
|
201
|
+
config.tenant_names = lambda do
|
202
|
+
if PandaPal::Organization < PandaPal::OrganizationConcerns::MultiDatabaseSharding
|
203
|
+
base_config = Apartment.connection_config
|
204
|
+
shard_configurations = Apartment.shard_configurations
|
205
|
+
|
206
|
+
PandaPal::Organization.all.to_a.each_with_object({}) do |org, hash|
|
207
|
+
shard = org.shard || "default"
|
208
|
+
hash[org.tenant_name] = shard == "default" ? base_config : shard_configurations[shard.downcase]
|
209
|
+
end
|
210
|
+
else
|
211
|
+
PandaPal::Organization.pluck(:name)
|
212
|
+
end
|
213
|
+
end
|
15
214
|
end
|
16
215
|
|
17
216
|
Rails.application.config.middleware.use Apartment::Elevators::Generic, lambda { |request|
|
data/lib/panda_pal/engine.rb
CHANGED
@@ -0,0 +1,85 @@
|
|
1
|
+
module PandaPal::SpecHelper
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
class_methods do
|
5
|
+
def with_multiple_shards(shards:, tenant_names: [], determine_shard: nil, transactional: true)
|
6
|
+
self.use_transactional_tests = transactional
|
7
|
+
|
8
|
+
original_settings = {}
|
9
|
+
|
10
|
+
before :all do
|
11
|
+
if defined?(DatabaseCleaner)
|
12
|
+
original_settings[:cleaner_strategy] = DatabaseCleaner.strategy
|
13
|
+
DatabaseCleaner.strategy = :truncation
|
14
|
+
end
|
15
|
+
|
16
|
+
Apartment.configure do |config|
|
17
|
+
original_settings[:with_multi_server_setup] = config.with_multi_server_setup
|
18
|
+
config.with_multi_server_setup = true
|
19
|
+
end
|
20
|
+
|
21
|
+
shards.each do |k, v|
|
22
|
+
Apartment.shard_configurations[k.to_s] = Apartment.connection_config.merge(database: v)
|
23
|
+
end
|
24
|
+
|
25
|
+
@existing_org_ids = PandaPal::Organization.pluck(:id)
|
26
|
+
|
27
|
+
Apartment::Tenant.reload!
|
28
|
+
|
29
|
+
clean_schemas(tenant_names)
|
30
|
+
end
|
31
|
+
|
32
|
+
after :all do
|
33
|
+
Apartment::Tenant.reset
|
34
|
+
|
35
|
+
Apartment.configure do |config|
|
36
|
+
config.with_multi_server_setup = false
|
37
|
+
end
|
38
|
+
|
39
|
+
shards.each do |k, _|
|
40
|
+
Apartment.shard_configurations.delete(k.to_s)
|
41
|
+
end
|
42
|
+
|
43
|
+
PandaPal::Organization.where.not(id: @existing_org_ids).delete_all
|
44
|
+
|
45
|
+
if defined?(DatabaseCleaner)
|
46
|
+
DatabaseCleaner.strategy = original_settings[:cleaner_strategy]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
before :each do
|
51
|
+
clean_schemas(tenant_names)
|
52
|
+
|
53
|
+
@each_existing_org_ids = PandaPal::Organization.pluck(:id)
|
54
|
+
|
55
|
+
if determine_shard
|
56
|
+
allow_any_instance_of(PandaPal::Organization).to receive(:tenant_name) do |org|
|
57
|
+
shard = determine_shard.call(org)
|
58
|
+
tenant = org.name
|
59
|
+
tenant = "#{shard}:#{tenant}" if shard.present?
|
60
|
+
tenant
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
after :each do
|
66
|
+
Apartment::Tenant.reset
|
67
|
+
PandaPal::Organization.where.not(id: @each_existing_org_ids).destroy_all
|
68
|
+
end
|
69
|
+
|
70
|
+
define_method :clean_schemas do |schemas|
|
71
|
+
# Clean known schemas if an Organization record does not exist for them
|
72
|
+
schema_to_fqt = schemas.index_by { |s| Apartment::Tenant.split_tenant(s)[1] }
|
73
|
+
schemas_to_clean = schema_to_fqt.keys - PandaPal::Organization.pluck(:name)
|
74
|
+
|
75
|
+
schemas_to_clean.each do |schema|
|
76
|
+
# fqt = schema_to_fqt[schema]
|
77
|
+
Apartment::Tenant.drop(schema) rescue nil
|
78
|
+
shards.each do |k, v|
|
79
|
+
Apartment::Tenant.drop("#{k}:#{schema}") rescue nil
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/lib/panda_pal/version.rb
CHANGED
data/panda_pal.gemspec
CHANGED
@@ -21,7 +21,7 @@ Gem::Specification.new do |s|
|
|
21
21
|
s.test_files = Dir["spec/**/*"].reject{|f| f =~ /\/log\//}
|
22
22
|
|
23
23
|
s.add_dependency "rails", ">= 4.2"
|
24
|
-
s.add_dependency 'ros-apartment', '~>
|
24
|
+
s.add_dependency 'ros-apartment', '~> 3.0'
|
25
25
|
s.add_dependency 'ims-lti', '~> 1.2.4'
|
26
26
|
s.add_dependency 'browser', '2.5.0'
|
27
27
|
s.add_dependency 'attr_encrypted', '~> 4.0.0'
|
@@ -30,11 +30,11 @@ Gem::Specification.new do |s|
|
|
30
30
|
s.add_dependency 'jwt'
|
31
31
|
s.add_dependency 'httparty'
|
32
32
|
|
33
|
-
s.add_development_dependency "rails", "~>
|
33
|
+
s.add_development_dependency "rails", "~> 7.0"
|
34
34
|
s.add_development_dependency 'pg'
|
35
35
|
s.add_development_dependency 'sidekiq', "< 7.0"
|
36
36
|
s.add_development_dependency 'sidekiq-scheduler'
|
37
|
-
s.add_development_dependency 'ros-apartment', '~>
|
37
|
+
s.add_development_dependency 'ros-apartment', '~> 3.0'
|
38
38
|
s.add_development_dependency 'ros-apartment-sidekiq', '~> 1.2'
|
39
39
|
s.add_development_dependency 'rspec-rails'
|
40
40
|
s.add_development_dependency 'factory_girl_rails'
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails_helper'
|
4
|
+
|
5
|
+
RSpec.describe PandaPal::Organization, type: :model do
|
6
|
+
context "across multiple DBs" do
|
7
|
+
with_multiple_shards(
|
8
|
+
shards: { alt: "panda_pal_test2" },
|
9
|
+
tenant_names: %w[on_primary alt:on_alt],
|
10
|
+
transactional: false,
|
11
|
+
determine_shard: ->(org) { org.name.include?("alt") ? "alt" : nil }
|
12
|
+
)
|
13
|
+
|
14
|
+
let!(:org1) { create(:panda_pal_organization, name: "on_primary") }
|
15
|
+
let!(:org2) { create(:panda_pal_organization, name: "on_alt") }
|
16
|
+
|
17
|
+
it "successfully creates tenants on multiple shards" do
|
18
|
+
expect(1).to eql(1)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "only creates schemas on the target shard" do
|
22
|
+
org1.switch_tenant do
|
23
|
+
schemas = PandaPal::ApiCall.connection.exec_query("SELECT schema_name FROM information_schema.schemata").pluck("schema_name")
|
24
|
+
expect(schemas).to include("on_primary")
|
25
|
+
expect(schemas).to_not include("on_alt")
|
26
|
+
end
|
27
|
+
org2.switch_tenant do
|
28
|
+
schemas = PandaPal::ApiCall.connection.exec_query("SELECT schema_name FROM information_schema.schemata").pluck("schema_name")
|
29
|
+
expect(schemas).to include("on_alt")
|
30
|
+
expect(schemas).to_not include("on_primary")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context "load_async" do
|
35
|
+
it "works across shards" do
|
36
|
+
qs = []
|
37
|
+
PandaPal::Organization.all.each do |org|
|
38
|
+
org.switch_tenant do
|
39
|
+
PandaPal::ApiCall.create!(logic: org.name)
|
40
|
+
qs << PandaPal::ApiCall.select("logic, pg_sleep(0.5)").load_async
|
41
|
+
end
|
42
|
+
end
|
43
|
+
calls = qs.map(&:to_a).flatten
|
44
|
+
expect(calls.pluck(:logic)).to match_array(["on_primary", "on_alt"])
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -14,8 +14,12 @@ development:
|
|
14
14
|
database: panda_pal_development
|
15
15
|
|
16
16
|
test:
|
17
|
-
|
18
|
-
|
17
|
+
test1:
|
18
|
+
<<: *default
|
19
|
+
database: panda_pal_test1
|
20
|
+
test2:
|
21
|
+
<<: *default
|
22
|
+
database: panda_pal_test2
|
19
23
|
|
20
24
|
production:
|
21
25
|
<<: *default
|
@@ -37,6 +37,11 @@ Rails.application.configure do
|
|
37
37
|
# Print deprecation notices to the stderr.
|
38
38
|
config.active_support.deprecation = :stderr
|
39
39
|
|
40
|
+
config.log_level = :debug
|
41
|
+
config.active_record.verbose_query_logs = true
|
42
|
+
config.active_record.async_query_executor = :multi_thread_pool
|
43
|
+
# config.active_record.global_executor_concurrency = 4
|
44
|
+
|
40
45
|
# Raises error for missing translations
|
41
46
|
# config.action_view.raise_on_missing_translations = true
|
42
47
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# This file is auto-generated from the current state of the database. Instead
|
2
|
+
# of editing this file, please use the migrations feature of Active Record to
|
3
|
+
# incrementally modify your database, and then regenerate this schema definition.
|
4
|
+
#
|
5
|
+
# Note that this schema.rb definition is the authoritative source for your
|
6
|
+
# database schema. If you need to create the application database on another
|
7
|
+
# system, you should be using db:schema:load, not running all the migrations
|
8
|
+
# from scratch. The latter is a flawed and unsustainable approach (the more migrations
|
9
|
+
# you'll amass, the slower it'll run and the greater likelihood for issues).
|
10
|
+
#
|
11
|
+
# It's strongly recommended that you check this file into your version control system.
|
12
|
+
|
13
|
+
ActiveRecord::Schema.define(version: 2022_07_21_095653) do
|
14
|
+
|
15
|
+
# These are extensions that must be enabled in order to support this database
|
16
|
+
enable_extension "plpgsql"
|
17
|
+
|
18
|
+
end
|
@@ -5,6 +5,10 @@ module PandaPal
|
|
5
5
|
RSpec.describe OrganizationConcerns::SettingsValidation, type: :model do
|
6
6
|
let!(:org) { create :panda_pal_organization }
|
7
7
|
|
8
|
+
after :all do
|
9
|
+
PandaPal.lti_options[:settings_structure] = nil
|
10
|
+
end
|
11
|
+
|
8
12
|
def set_test_settings_structure
|
9
13
|
PandaPal.lti_options = {
|
10
14
|
title: 'Test App',
|
File without changes
|
data/spec/rails_helper.rb
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
ActiveRecord::Migration.maintain_test_schema!
|
2
2
|
|
3
|
+
require "panda_pal/spec_helper"
|
4
|
+
|
3
5
|
RSpec.configure do |config|
|
6
|
+
config.include PandaPal::SpecHelper
|
7
|
+
|
4
8
|
# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
|
5
9
|
config.fixture_path = "#{::Rails.root}/spec/fixtures"
|
6
10
|
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: panda_pal
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 5.
|
4
|
+
version: 5.12.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Instructure CustomDev
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-11-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -30,14 +30,14 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '3.0'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '3.0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: ims-lti
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -142,14 +142,14 @@ dependencies:
|
|
142
142
|
requirements:
|
143
143
|
- - "~>"
|
144
144
|
- !ruby/object:Gem::Version
|
145
|
-
version: '
|
145
|
+
version: '7.0'
|
146
146
|
type: :development
|
147
147
|
prerelease: false
|
148
148
|
version_requirements: !ruby/object:Gem::Requirement
|
149
149
|
requirements:
|
150
150
|
- - "~>"
|
151
151
|
- !ruby/object:Gem::Version
|
152
|
-
version: '
|
152
|
+
version: '7.0'
|
153
153
|
- !ruby/object:Gem::Dependency
|
154
154
|
name: pg
|
155
155
|
requirement: !ruby/object:Gem::Requirement
|
@@ -198,14 +198,14 @@ dependencies:
|
|
198
198
|
requirements:
|
199
199
|
- - "~>"
|
200
200
|
- !ruby/object:Gem::Version
|
201
|
-
version: '
|
201
|
+
version: '3.0'
|
202
202
|
type: :development
|
203
203
|
prerelease: false
|
204
204
|
version_requirements: !ruby/object:Gem::Requirement
|
205
205
|
requirements:
|
206
206
|
- - "~>"
|
207
207
|
- !ruby/object:Gem::Version
|
208
|
-
version: '
|
208
|
+
version: '3.0'
|
209
209
|
- !ruby/object:Gem::Dependency
|
210
210
|
name: ros-apartment-sidekiq
|
211
211
|
requirement: !ruby/object:Gem::Requirement
|
@@ -282,7 +282,7 @@ dependencies:
|
|
282
282
|
- - '='
|
283
283
|
- !ruby/object:Gem::Version
|
284
284
|
version: 2.7.1
|
285
|
-
description:
|
285
|
+
description:
|
286
286
|
email:
|
287
287
|
- pseng@instructure.com
|
288
288
|
executables: []
|
@@ -311,9 +311,11 @@ files:
|
|
311
311
|
- app/lib/panda_pal/lti_jwt_validator.rb
|
312
312
|
- app/models/panda_pal/api_call.rb
|
313
313
|
- app/models/panda_pal/organization.rb
|
314
|
+
- app/models/panda_pal/organization_concerns/multi_database_sharding.rb
|
314
315
|
- app/models/panda_pal/organization_concerns/organization_builder.rb
|
315
316
|
- app/models/panda_pal/organization_concerns/settings_validation.rb
|
316
317
|
- app/models/panda_pal/organization_concerns/task_scheduling.rb
|
318
|
+
- app/models/panda_pal/organization_concerns/tenant_handling.rb
|
317
319
|
- app/models/panda_pal/panda_pal_record.rb
|
318
320
|
- app/models/panda_pal/platform.rb
|
319
321
|
- app/models/panda_pal/platform/canvas.rb
|
@@ -343,21 +345,23 @@ files:
|
|
343
345
|
- lib/panda_pal/helpers/secure_headers.rb
|
344
346
|
- lib/panda_pal/helpers/session_replacement.rb
|
345
347
|
- lib/panda_pal/plugins.rb
|
348
|
+
- lib/panda_pal/spec_helper.rb
|
346
349
|
- lib/panda_pal/version.rb
|
347
350
|
- lib/tasks/panda_pal_tasks.rake
|
348
351
|
- panda_pal.gemspec
|
349
352
|
- spec/controllers/panda_pal/api_call_controller_spec.rb
|
353
|
+
- spec/core/apartment_multidb_spec.rb
|
350
354
|
- spec/dummy/README.rdoc
|
351
355
|
- spec/dummy/Rakefile
|
352
356
|
- spec/dummy/app/assets/javascripts/application.js
|
353
357
|
- spec/dummy/app/assets/stylesheets/application.css
|
358
|
+
- spec/dummy/app/bin/bundle
|
359
|
+
- spec/dummy/app/bin/rails
|
360
|
+
- spec/dummy/app/bin/rake
|
361
|
+
- spec/dummy/app/bin/setup
|
354
362
|
- spec/dummy/app/controllers/application_controller.rb
|
355
363
|
- spec/dummy/app/helpers/application_helper.rb
|
356
364
|
- spec/dummy/app/views/layouts/application.html.erb
|
357
|
-
- spec/dummy/bin/bundle
|
358
|
-
- spec/dummy/bin/rails
|
359
|
-
- spec/dummy/bin/rake
|
360
|
-
- spec/dummy/bin/setup
|
361
365
|
- spec/dummy/config.ru
|
362
366
|
- spec/dummy/config/application.rb
|
363
367
|
- spec/dummy/config/boot.rb
|
@@ -377,6 +381,7 @@ files:
|
|
377
381
|
- spec/dummy/config/routes.rb
|
378
382
|
- spec/dummy/config/secrets.yml
|
379
383
|
- spec/dummy/db/schema.rb
|
384
|
+
- spec/dummy/db/test2_schema.rb
|
380
385
|
- spec/dummy/public/404.html
|
381
386
|
- spec/dummy/public/422.html
|
382
387
|
- spec/dummy/public/500.html
|
@@ -394,7 +399,7 @@ homepage: http://instructure.com
|
|
394
399
|
licenses:
|
395
400
|
- MIT
|
396
401
|
metadata: {}
|
397
|
-
post_install_message:
|
402
|
+
post_install_message:
|
398
403
|
rdoc_options: []
|
399
404
|
require_paths:
|
400
405
|
- lib
|
@@ -409,52 +414,54 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
409
414
|
- !ruby/object:Gem::Version
|
410
415
|
version: '0'
|
411
416
|
requirements: []
|
412
|
-
rubygems_version: 3.
|
413
|
-
signing_key:
|
417
|
+
rubygems_version: 3.5.16
|
418
|
+
signing_key:
|
414
419
|
specification_version: 4
|
415
420
|
summary: LTI mountable engine
|
416
421
|
test_files:
|
417
|
-
- spec/
|
418
|
-
- spec/
|
419
|
-
- spec/dummy/
|
422
|
+
- spec/controllers/panda_pal/api_call_controller_spec.rb
|
423
|
+
- spec/core/apartment_multidb_spec.rb
|
424
|
+
- spec/dummy/README.rdoc
|
425
|
+
- spec/dummy/Rakefile
|
420
426
|
- spec/dummy/app/assets/javascripts/application.js
|
421
427
|
- spec/dummy/app/assets/stylesheets/application.css
|
428
|
+
- spec/dummy/app/bin/bundle
|
429
|
+
- spec/dummy/app/bin/rails
|
430
|
+
- spec/dummy/app/bin/rake
|
431
|
+
- spec/dummy/app/bin/setup
|
432
|
+
- spec/dummy/app/controllers/application_controller.rb
|
422
433
|
- spec/dummy/app/helpers/application_helper.rb
|
423
|
-
- spec/dummy/
|
424
|
-
- spec/dummy/bin/setup
|
425
|
-
- spec/dummy/bin/bundle
|
426
|
-
- spec/dummy/bin/rails
|
427
|
-
- spec/dummy/config/secrets.yml
|
428
|
-
- spec/dummy/config/routes.rb
|
429
|
-
- spec/dummy/config/locales/en.yml
|
430
|
-
- spec/dummy/config/environments/production.rb
|
431
|
-
- spec/dummy/config/environments/development.rb
|
432
|
-
- spec/dummy/config/environments/test.rb
|
433
|
-
- spec/dummy/config/environment.rb
|
434
|
+
- spec/dummy/app/views/layouts/application.html.erb
|
434
435
|
- spec/dummy/config/application.rb
|
435
|
-
- spec/dummy/config/database.yml
|
436
436
|
- spec/dummy/config/boot.rb
|
437
|
+
- spec/dummy/config/database.yml
|
438
|
+
- spec/dummy/config/environment.rb
|
439
|
+
- spec/dummy/config/environments/development.rb
|
440
|
+
- spec/dummy/config/environments/production.rb
|
441
|
+
- spec/dummy/config/environments/test.rb
|
437
442
|
- spec/dummy/config/initializers/backtrace_silencers.rb
|
438
|
-
- spec/dummy/config/initializers/
|
443
|
+
- spec/dummy/config/initializers/cookies_serializer.rb
|
439
444
|
- spec/dummy/config/initializers/filter_parameter_logging.rb
|
445
|
+
- spec/dummy/config/initializers/inflections.rb
|
446
|
+
- spec/dummy/config/initializers/mime_types.rb
|
440
447
|
- spec/dummy/config/initializers/session_store.rb
|
441
448
|
- spec/dummy/config/initializers/wrap_parameters.rb
|
442
|
-
- spec/dummy/config/
|
443
|
-
- spec/dummy/config/
|
449
|
+
- spec/dummy/config/locales/en.yml
|
450
|
+
- spec/dummy/config/routes.rb
|
451
|
+
- spec/dummy/config/secrets.yml
|
444
452
|
- spec/dummy/config.ru
|
445
|
-
- spec/dummy/
|
446
|
-
- spec/dummy/
|
453
|
+
- spec/dummy/db/schema.rb
|
454
|
+
- spec/dummy/db/test2_schema.rb
|
455
|
+
- spec/dummy/public/404.html
|
447
456
|
- spec/dummy/public/422.html
|
448
457
|
- spec/dummy/public/500.html
|
449
|
-
- spec/dummy/public/
|
450
|
-
- spec/
|
451
|
-
- spec/
|
458
|
+
- spec/dummy/public/favicon.ico
|
459
|
+
- spec/factories/panda_pal_organizations.rb
|
460
|
+
- spec/factories/panda_pal_sessions.rb
|
461
|
+
- spec/models/panda_pal/api_call_spec.rb
|
452
462
|
- spec/models/panda_pal/organization/settings_validation_spec.rb
|
453
463
|
- spec/models/panda_pal/organization/task_scheduling_spec.rb
|
454
|
-
- spec/models/panda_pal/session_spec.rb
|
455
|
-
- spec/models/panda_pal/api_call_spec.rb
|
456
464
|
- spec/models/panda_pal/organization_spec.rb
|
457
|
-
- spec/
|
458
|
-
- spec/factories/panda_pal_organizations.rb
|
459
|
-
- spec/controllers/panda_pal/api_call_controller_spec.rb
|
465
|
+
- spec/models/panda_pal/session_spec.rb
|
460
466
|
- spec/rails_helper.rb
|
467
|
+
- spec/spec_helper.rb
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|