Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto reload for local mode #706

Closed
wants to merge 18 commits into from
Closed

Auto reload for local mode #706

wants to merge 18 commits into from

Conversation

vtitor
Copy link
Contributor

@vtitor vtitor commented Feb 8, 2018

I have implemented auto reload feature by #316 request. It doesn't have any dependencies (according to #658 remarks).
@jamesls, @stealthycoin Could you help me merge this PR as soon as possible, please?

@vtitor
Copy link
Contributor Author

vtitor commented Feb 28, 2018

@jamesls, @stealthycoin Hi guys! If I have some mistakes in this pr could you please tell me about them

Copy link
Contributor

@stealthycoin stealthycoin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for taking the time to make this pull request. It's been something I've been wanting for awhile as well and I'd like to see it get shipped.

I had some comments and questions. Scattered throughout. The main thing besides the below comments would be to get some tests added.

@@ -304,7 +304,7 @@ valid-metaclass-classmethod-first-arg=mcs

# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,_fields,_replace,_source,_make
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer not to reach into the _thread if at all possible. Are there other options?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, there are no other options.

def reload(self):
# type: () -> None
self.triggered = True
if sys.platform == 'win32':
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We prefer to have all platform specific logic in the compat.py file in one place so we don't need to think about it here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I'll fix this.

@@ -79,11 +80,14 @@ def cli(ctx, project_dir, debug=False):
@click.option('--port', default=8000, type=click.INT)
@click.option('--stage', default=DEFAULT_STAGE_NAME,
help='Name of the Chalice stage for the local server to use.')
@click.option('--autoreload', is_flag=True)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like it should be turned on by default so people don't need to discover the functionality, it just magically works. We could flip it and let them disable it if they want to for some reason. --no-autoreload. Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes sense.

factory = ctx.obj['factory'] # type: CLIFactory
run_local_server(factory, host, port, stage, os.environ)
with Reloader(autoreload):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems a little bit strange to me to pass False to class called Reloader. Could we refactor this to just not use the Reloader context manager if its disabled?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, I'll fix it.

def run(self):
# type: () -> None
while True:
time.sleep(AUTORELOAD_INTERVAL)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be nice as a parameter that can be supplied to the reloader.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep.


def find_changes(self):
# type: () -> bool
for module in list(sys.modules.values()):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • No need to cast to list here.
  • Can we scope this to just the files that are in their Chalice project?

Copy link
Contributor Author

@vtitor vtitor Mar 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to cast to list here.

Why? RuntimeError: dictionary changed size during iteration

Can we scope this to just the files that are in their Chalice project?

We can.. but I think it's better to check all modules.

subprocess.Popen(sys.argv, close_fds=True)
six.moves._thread.interrupt_main()
else:
os.execv(sys.executable, [sys.executable] + sys.argv)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't had the chance to try out this branch yet but I'm curious about this approach and how it compares to managing a subprocess yourself, or hot reloading the python code. For example:
https://docs.python.org/3.4/library/importlib.html#importlib.reload
https://docs.python.org/3.2/library/imp.html#imp.load_module
etc...

I'm mainly trying to think of a more testable approach that would not need any platform specific logic. Would like to hear your thoughts on why you chose this approach.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reload python module at runtime looks like a bad idea to me (because it causes inconsistently application state)

# type: (bool) -> None
super(Reloader, self).__init__()
self.autoreload = autoreload
self.triggered = False
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recognize that this is whether or not the process has ever reloaded. But can you give some clarification on why this is important to keep track of?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to have additional behavior when interrupting for the reloaded process in windows (raise SystemExit).

self.triggered = True
if sys.platform == 'win32':
subprocess.Popen(sys.argv, close_fds=True)
six.moves._thread.interrupt_main()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain why this is needed on windows?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

execv in windows is completely different than in linux. Briefly, it's necessary to close the opened file descriptors.

@codecov-io
Copy link

codecov-io commented May 3, 2018

Codecov Report

Merging #706 into master will decrease coverage by 0.92%.
The diff coverage is 37.09%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #706      +/-   ##
==========================================
- Coverage   94.79%   93.86%   -0.93%     
==========================================
  Files          21       22       +1     
  Lines        3707     3767      +60     
  Branches      478      486       +8     
==========================================
+ Hits         3514     3536      +22     
- Misses        130      168      +38     
  Partials       63       63
Impacted Files Coverage Δ
chalice/constants.py 100% <100%> (ø) ⬆️
chalice/cli/reload.py 31.11% <31.11%> (ø)
chalice/compat.py 40.9% <42.85%> (+0.36%) ⬆️
chalice/cli/__init__.py 78.04% <55.55%> (-0.74%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 46cbb73...02da148. Read the comment docs.

@vtitor
Copy link
Contributor Author

vtitor commented May 9, 2018

@jamesls, @stealthycoin Could you review my changes, please?

@jamesls
Copy link
Member

jamesls commented May 14, 2018

Taking a look now. I haven't had a chance to look at this PR in general (thanks @stealthycoin for the feedback so far), but I think the feature itself would be good to have in chalice.

Was there a discussion about pulling in some existing library that can watch for file changes? Seems like it would be preferable to use something based on inotify/fsevents instead of polling for changes.

@jamesls
Copy link
Member

jamesls commented May 15, 2018

Hi, I know there's been a lot of requests for this feature so I'll go ahead and pull in these changes and make a few adjustments before merging.

@vtitor
Copy link
Contributor Author

vtitor commented May 15, 2018

@jamesls Thanks, I will be glad to help you. About inotify/fsevents - sounds good to me, but I guess it would be better to deliver working autoreload feature as soon as possible and then improve it in a separate pr (because as you said there are a lot of requests for this stuff).

jamesls added a commit to jamesls/chalice that referenced this pull request May 18, 2018
This adds a reloader to chalice local that's enabled by
default.  A new library, watchdog, is pulled in as a dependency
of chalice.  This allows us to efficiently monitor files
for changes using whatever the most efficient OS specific mechanism
(FSEvents, inotify, etc).

The general approach is borrowed from other frameworks such as
django and flask but the implementation is somewhat different.
Two processes are spun up, a worker process and a monitoring
process.  The monitoring process spawns the worker process and
restarts it if it exits with a special RC.  The worker process
launches the dev server in one thread and a file monitor in
a separate thread.  When files are detected it shuts down the
server and exits with a special RC.

There's a few differences from the other approaches:

* Avoid the use of sys.exit() directly in the reloader.  This
  made the code hard to test and seems odd that a library
  function is calling sys.exit().  However there is a call
  to `sys.exit()` in the `local()` function because this is
  the recommended way in click for a command to exit with
  a specific RC (see the comments for more info).
* We only monitor the project dir for changes.  The other
  reloaders appear to monitor everything in sys.path, which
  seems like overkill here.  The way I see it, stuff on
  sys.path would only be modified if you're installing new
  packages, in which case you'll have to modify requirements.txt
  as well as something in your app.py (in order to import the
  newly installed module).  I dug around their repos for a bit
  and couldn't find a rationale as to why that much
  monitoring is needed.

A note on coverage: because the reloader works by launching
child processes, this isn't going to be tracked via
coverage, so I expect codecov to fail.
There is some support for tracking coverage across subprocesses, but it
involved messing with sitecustomize.py or .pth files.
See http://coverage.readthedocs.io/en/coverage-4.2/subprocess.html
for more info on that.

Supercedes aws#706.
jamesls added a commit to jamesls/chalice that referenced this pull request May 18, 2018
This adds a reloader to chalice local that's enabled by
default.  A new library, watchdog, is pulled in as a dependency
of chalice.  This allows us to efficiently monitor files
for changes using whatever the most efficient OS specific mechanism
(FSEvents, inotify, etc).

The general approach is borrowed from other frameworks such as
django and flask but the implementation is somewhat different.
Two processes are spun up, a worker process and a monitoring
process.  The monitoring process spawns the worker process and
restarts it if it exits with a special RC.  The worker process
launches the dev server in one thread and a file monitor in
a separate thread.  When files are detected it shuts down the
server and exits with a special RC.

There's a few differences from the other approaches:

* Avoid the use of sys.exit() directly in the reloader.  This
  made the code hard to test and seems odd that a library
  function is calling sys.exit().  However there is a call
  to `sys.exit()` in the `local()` function because this is
  the recommended way in click for a command to exit with
  a specific RC (see the comments for more info).
* We only monitor the project dir for changes.  The other
  reloaders appear to monitor everything in sys.path, which
  seems like overkill here.  The way I see it, stuff on
  sys.path would only be modified if you're installing new
  packages, in which case you'll have to modify requirements.txt
  as well as something in your app.py (in order to import the
  newly installed module).  I dug around their repos for a bit
  and couldn't find a rationale as to why that much
  monitoring is needed.

A note on coverage: because the reloader works by launching
child processes, this isn't going to be tracked via
coverage, so I expect codecov to fail.
There is some support for tracking coverage across subprocesses, but it
involved messing with sitecustomize.py or .pth files.
See http://coverage.readthedocs.io/en/coverage-4.2/subprocess.html
for more info on that.

Supercedes aws#706.
jamesls added a commit to jamesls/chalice that referenced this pull request May 18, 2018
This adds a reloader to chalice local that's enabled by
default.  A new library, watchdog, is pulled in as a dependency
of chalice.  This allows us to efficiently monitor files
for changes using whatever the most efficient OS specific mechanism
(FSEvents, inotify, etc).

The general approach is borrowed from other frameworks such as
django and flask but the implementation is somewhat different.
Two processes are spun up, a worker process and a monitoring
process.  The monitoring process spawns the worker process and
restarts it if it exits with a special RC.  The worker process
launches the dev server in one thread and a file monitor in
a separate thread.  When files are detected it shuts down the
server and exits with a special RC.

There's a few differences from the other approaches:

* Avoid the use of sys.exit() directly in the reloader.  This
  made the code hard to test and seems odd that a library
  function is calling sys.exit().  However there is a call
  to `sys.exit()` in the `local()` function because this is
  the recommended way in click for a command to exit with
  a specific RC (see the comments for more info).
* We only monitor the project dir for changes.  The other
  reloaders appear to monitor everything in sys.path, which
  seems like overkill here.  The way I see it, stuff on
  sys.path would only be modified if you're installing new
  packages, in which case you'll have to modify requirements.txt
  as well as something in your app.py (in order to import the
  newly installed module).  I dug around their repos for a bit
  and couldn't find a rationale as to why that much
  monitoring is needed.

A note on coverage: because the reloader works by launching
child processes, this isn't going to be tracked via
coverage, so I expect codecov to fail.
There is some support for tracking coverage across subprocesses, but it
involved messing with sitecustomize.py or .pth files.
See http://coverage.readthedocs.io/en/coverage-4.2/subprocess.html
for more info on that.

Supercedes aws#706.
jamesls added a commit to jamesls/chalice that referenced this pull request May 18, 2018
This adds a reloader to chalice local that's enabled by
default.  A new library, watchdog, is pulled in as a dependency
of chalice.  This allows us to efficiently monitor files
for changes using whatever the most efficient OS specific mechanism
(FSEvents, inotify, etc).

The general approach is borrowed from other frameworks such as
django and flask but the implementation is somewhat different.
Two processes are spun up, a worker process and a monitoring
process.  The monitoring process spawns the worker process and
restarts it if it exits with a special RC.  The worker process
launches the dev server in one thread and a file monitor in
a separate thread.  When files are detected it shuts down the
server and exits with a special RC.

There's a few differences from the other approaches:

* Avoid the use of sys.exit() directly in the reloader.  This
  made the code hard to test and seems odd that a library
  function is calling sys.exit().  However there is a call
  to `sys.exit()` in the `local()` function because this is
  the recommended way in click for a command to exit with
  a specific RC (see the comments for more info).
* We only monitor the project dir for changes.  The other
  reloaders appear to monitor everything in sys.path, which
  seems like overkill here.  The way I see it, stuff on
  sys.path would only be modified if you're installing new
  packages, in which case you'll have to modify requirements.txt
  as well as something in your app.py (in order to import the
  newly installed module).  I dug around their repos for a bit
  and couldn't find a rationale as to why that much
  monitoring is needed.

A note on coverage: because the reloader works by launching
child processes, this isn't going to be tracked via
coverage, so I expect codecov to fail.
There is some support for tracking coverage across subprocesses, but it
involved messing with sitecustomize.py or .pth files.
See http://coverage.readthedocs.io/en/coverage-4.2/subprocess.html
for more info on that.

Supercedes aws#706.
@jamesls
Copy link
Member

jamesls commented May 21, 2018

Merged via #846. This will go out in the next release. Thanks again for the original PR.

@jamesls jamesls closed this May 21, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants