panda_pal 5.11.0 → 5.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +82 -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: ab899936461dc59208ae6afef28264feda7a75927798e158ca1ee8381b2ea463
|
4
|
+
data.tar.gz: 20424ac6bef9bede8af9e58e8a1393267b0966f96cf56dd8ae8ed0e5b486420a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 85041955aabe8255fcf0a4e3cbcfce3c55c690e0d66d74ac511d6b4d70825d796a62a18fe3ac3e0b5dc66e9ddc865c5b45f672940d92c15a1cc38a7ac350ad91
|
7
|
+
data.tar.gz: bcbe2513a4b4916af671f5a913036e0154dd474d510351f6325a2f264d03c1dac10adc4c48bc060c40faafdc798268664c48e2c32943dd4ed3d0dcda076b7ff3
|
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,82 @@
|
|
1
|
+
module PandaPal::SpecHelper
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
class_methods do
|
5
|
+
def with_multiple_shards(shards:, preclean_schemas: [], 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(preclean_schemas)
|
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(preclean_schemas) unless transactional
|
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
|
+
PandaPal::Organization.where(name: schemas).delete_all
|
72
|
+
|
73
|
+
schemas.each do |schema|
|
74
|
+
Apartment::Tenant.drop(schema) rescue nil
|
75
|
+
shards.each do |k, v|
|
76
|
+
Apartment::Tenant.drop("#{k}:#{schema}") rescue nil
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
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
|
+
preclean_schemas: %w[on_primary 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.0
|
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-26 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
|