Backward compatibility without tears #6912
Replies: 3 comments
-
Using compatibility moduleCompat is run automatically at the start of a tarantool session. You can access it as
|
Beta Was this translation helpful? Give feedback.
-
SummaryThe usual way to handle compatibility problems is to introduce an option for new behavior and leave the old one by default. It is not always the perfect way. Sometimes we want to keep the old behavior for existing applications and offer the new one by default for new ones. For example, the old behavior is known to be problematic / less safe / does not correspond to user expectations. In contrast, the user doesn’t always read all the documentation and often assumes good defaults. It was decided to introduce a compatibility module to provide a direct way to deprecate unwanted behavior.
on the first two stages, user can toggle options via the interface and change the behavior according to his needs, on the last stage old behavior is removed from codebase and option is marked as obsolete. As Options will be switched to the next stage in major releases, this way developers would be able to adapt to the new standard behavior and test it before switching to the next release. If something is broken by the new tarantool version, developer would still have a way to fix it by a simple config change (explicitly select old behavior). Consider example below:
TutorialThe options list is serialized in the interactive console with additional details for user convenience:
tarantool> compat = require('tarantool').compat
---
...
tarantool> compat
---
- - json_escape_forward_slash: new
- - option_2: old
- - option_default_old: default (old)
- - option_default_new: default (new)
...
tarantool> compat.option_default_new
---
- current: old
default: new
brief: <...>
obsolete: false -- explicit
...
tarantool> compat.json_escape_forward_slash = 'old'
---
...
tarantool> compat{json_escape_forward_slash = 'new', option_2 = 'default'}
---
...
tarantool> compat.option_2 = 'default'
---
...
tarantool> compat.option_2
---
- current: default
- default: new
- brief: <...>
...
tarantool> compat({
> obsolete_set_explicitly = 'new',
> option_set_old = 'old',
> option_set_new = 'new'
> })
---
...
tarantool> compat
---
- - option_set_old: old
- - option_set_new: new
- - option_default_old: default (old)
- - option_default_new: default (new)
- - obsolete_option_default: default (new)
- - obsolete_set_explicitly: new
...
# nil does output obsolete unset options as 'default'
tarantool> compat.dump()
---
- require('tarantool').compat({
option_set_old = 'old',
option_set_new = 'new',
option_default_old = 'default',
option_default_new = 'default',
obsolete_option_default = 'default', -- obsolete since X.Y.Z
obsolete_set_explicitly = 'new', -- obsolete since X.Y.Z
})
...
# 'current' is same as nil with default set to current values
tarantool> compat.dump('current')
---
- require('tarantool').compat({
option_set_old = 'old',
option_set_new = 'new',
option_default_old = 'old',
option_default_new = 'new',
obsolete_option_default = 'new', -- obsolete since X.Y.Z
obsolete_set_explicitly = 'new', -- obsolete since X.Y.Z
})
...
# 'new' outputs obsolete as 'new'.
tarantool> compat.dump('new')
---
- require('tarantool').compat({
option_set_old = 'new',
option_set_new = 'new',
option_default_old = 'new',
option_default_new = 'new',
obsolete_option_default = 'new', -- obsolete since X.Y.Z
obsolete_set_explicitly = 'new', -- obsolete since X.Y.Z
})
...
# 'old' outputs obsolete options as 'new'.
tarantool> compat.dump('old')
---
- require('tarantool').compat({
option_set_old = 'old',
option_set_new = 'old',
option_default_old = 'old',
option_default_new = 'old',
obsolete_option_default = 'new', -- obsolete since X.Y.Z
obsolete_set_explicitly = 'new', -- obsolete since X.Y.Z
})
...
# 'default' does output obsolete options as default.
tarantool> dump('default')
---
- require('tarantool').compat({
option_set_old = 'default',
option_set_new = 'default',
option_default_old = 'default',
option_default_new = 'default',
obsolete_option_default = 'default', -- obsoleted since X.Y.Z
obsolete_set_explicitly = 'default', -- obsoleted since X.Y.Z
})
...
tarantool> compat.dump('new')
---
- require('tarantool').compat({
option_2 = 'new',
json_escape_forward_slash = 'new',
})
...
tarantool> require('tarantool').compat({
option_2 = 'new',
json_escape_forward_slash = 'new',
})
---
...
tarantool> compat
---
- - json_escape_forward_slash: new
- - option_2: new
...
tarantool> compat.add_option{
name = 'option_4',
default = 'new',
brief = "<...>",
obsolete = false, -- You can explicitly mark option as non-obsolete.
action = function(is_new)
print(("option_4 action was called with is_new = %s!"):format(is_new))
end,
run_action = true
}
option_4 postaction was called with is_new = true!
---
...
tarantool> compat.add_option{ -- hot reload of option_4
name = 'option_4',
default = 'old', -- different default
brief = "<...>",
action = function(is_new)
print(("new option_4 action was called with is_new = %s!"):format(is_new))
end
}
---
... -- action is not called by default |
Beta Was this translation helpful? Give feedback.
-
#7060 is pushed into 2.11 and will come with first 2.11 release. |
Beta Was this translation helpful? Give feedback.
-
The usual way to handle compatibility problems is to introduce an option and leave the old behaviour by default. It is not always the perfect way.
Sometimes we want to keep the old behaviour for existing applications and offer the new one by default for new applications. For example the old behaviour is known to be problematic / less safe / does not correspond to expectations of a user. A user not always read all the documentation and often assume good defaults.
I had an idea: introduce a 'compatibility profile' thing. Basically it is a global options table:
With helper functions, getters and setters:
(The table format, function names and so on are just to illustrate the idea and likely not the best choice.)
Just global table is not the all story. To make it a solution for the problem, we should implement the following:
tarantool.compat.restore()
call in the documentation. The same for application examples / templates (including ones provided by a CLI).Doubts.
An application may stitch many very different parts: modules, frameworks, integrations with external services, domain specific logic. It may be not easy (and even impossible in practice) to verify whether all parts of the application are ready for enabling a new behaviour in a built-in module.
OTOH, the idea has no sense if we'll set different compatibility options for different parts of the applications: this way it is the same as just a runtime option and does not introduce any simplification for a user. In other words, this way we lack of one place to tweak the compatibility options.
It is not a simple problem and I doubt that it has an always working / automatic solution. However there is a light beam. Since we have a documentation URL for each compatibility entry, we can:
What is also worth to say there. There is an opinion that all compatibility problems were resolved by semver. It is far beyond the truth for library alike projects: ones, which are base ground for other libraries and applications.
A project maintainers have the responsibility to do their best to don't let existing users update its code (or validate, whether it needs an update) too often. Depending of a nature of the project, this period varies from half of a year to several years.
Semantic versioning provides a way to deliver information that an action is required to update the software, nothing more. It does not provide an indulgence to change anything you want to change (if you have users). The responsibility can't disappear, because some magic numbers are changed.
In practice, even a new major version can't change anything. Well, it breaks a particular set of functionality to provide something better. But not more: otherwise it is pure nightmare for a user, to understand whether (s)he can update or can't.
Inspired by #6200.
Beta Was this translation helpful? Give feedback.
All reactions