Mike Slinn

Connoisseur of Technology

Debugging JVM Programs on Heroku

2012-09-28 / All Blog posts

Debugging Java Applications

Sometimes there is no substitute for debugging a remote application. The Java virtual machine provides the JPDA facility for this. JPDA is flexible, and can be configured in a variety of ways. Two attachment mechanisms are supported for debugging remote applications: inbound connections, whereby a debugging process on your machine attaches to a remote process via designated port at a specific IP address, and outbound connections, whereby a debugging process on your local machine listens to a designated port for a remote process to attach to it. JPDA can be used by all JVM-based languages, such as Java, Scala, Groovy and Clojure.

Heroku only allows one incoming port, so because an incoming port is used by Heroku to connect to the hosted app, debug connections originating from your IDE will not succeed. Instead, you must set up your Heroku application to initiate an outbound connection for debugging. This cannot be done if your app uses more than one dyno, so you must scale your Heroku back to one dyno before you can remotely debug it, like this:
heroku scale web=1

A Heroku app can initiate an outbound connection for debugging with or without a proxy server. The examples below use the Java debugger (jdb) and IntelliJ IDEA, but you could equally well set up a debug configuration for Eclipse in a similar manner. The settings below were used with a Play 2 application with Java and Scala controller classes.

Tip: heroku restart will restart your Heroku app using your existing slug. This accomplishes the same thing as:
heroku scale web=0
heroku scale web=1

The other way you can restart your app is by building a new slug, which then automatically starts. You can do this by checking in a bogus file:

date>ignoreme.txt; git add ignoreme.txt; git push heroku

Warning: Your Heroku app will crash if there is no listening process. Use the heroku logs command to check for a crash.

$ heroku logs
2012-09-29T18:20:53+00:00 heroku[web.1]: Starting process with command `target/start -Dhttp.port=${PORT} ${JAVA_OPTS}`
2012-09-29T18:20:55+00:00 app[web.1]: ERROR: transport error 202: connect failed: Connection refused
2012-09-29T18:20:55+00:00 app[web.1]: ERROR: JDWP Transport dt_socket failed to initialize, TRANSPORT_INIT(510)
2012-09-29T18:20:55+00:00 app[web.1]: JDWP exit error AGENT_ERROR_TRANSPORT_INIT(197): No transports initialized [../../../src/share/back/debugInit.c:741]
2012-09-29T18:20:55+00:00 app[web.1]: FATAL ERROR in native method: JDWP No transports initialized, jvmtiError=AGENT_ERROR_TRANSPORT_INIT(197)
2012-09-29T18:20:56+00:00 heroku[web.1]: Process exited with status 134
2012-09-29T18:20:56+00:00 heroku[web.1]: State changed from starting to crashed

Remote Debugging Without a Proxy Server

If your local machine is accessible from the Internet at an IP address (a domain such as blah.no-ip.info would work equally well):

jdb -listen 9999& # Do not type this line if you are using an IDE
# If using an IDE, start debugging now

# Assumes that eth0 accesses the Internet; only works if you are not behind NAT
IPADDR=`ifconfig eth0|grep "inet addr"|awk -F: '{print $2}'|awk '{print $1}'`

# If you are behind NAT, you will need to use a dynamic DNS and set IPADDR to the machine name instead:
IPADDR=mycomputer.no-ip.info # modify to suit

# This is a really long line. I wrapped it, but you should not:
heroku config:add JAVA_OPTS='-Xdebug
  -Xrunjdwp:transport=dt_socket,address=$IPADDR:9999
  -Xms512M -Xmx1024M -Xss1M -XX:+CMSClassUnloadingEnabled
  -XX:MaxPermSize=256M -XX:+UseCompressedOops'

heroku restart

Here is what the run configuration for IntelliJ IDEA looks like. Note that debugger mode is set to listen, which implies server="n". You need to launch this run configuration before restarting your Heroku app.

Bash Script Implementation

I put the bash scripts in a gist:

Remote Debugging With a Proxy Server

If your local machine is hidden from the Internet by a proxy server at blah.domain.com, open a tunnel to it from your local machine, and listen to it:

# Assumes that eth0 accesses the Internet; only works if you are not behind NAT
IPADDR=`ifconfig eth0|grep "inet addr"|awk -F: '{print $2}'|awk '{print $1}'`

# If you are behind NAT, you will need to use a dynamic DNS and set IPADDR to the machine name instead:
IPADDR=mycomputer.no-ip.info # modify to suit

ssh -NR *:9999:localhost:9999 $IPADDR&

jdb -listen 9999& # Do not type this line if you are using an IDE

If using an IDE, start debugging now.

# This is a really long line. I wrapped it, but you should not:
heroku config:add JAVA_OPTS='-Xdebug
  -Xrunjdwp:transport=dt_socket,address=$IPADDR:9999
  -Xms512M -Xmx1024M -Xss1M -XX:+CMSClassUnloadingEnabled
  -XX:MaxPermSize=256M -XX:+UseCompressedOops'

heroku restart

Disabling Remote Debugging

The JAVA_OPTS value set earlier will remain in effect across multiple restarts of your Heroku app until you change it. To disable remote debugging, redefine the environment variable without the -Xdebug and -Xrunjdwp options:

# This is a really long line. I wrapped it, but you should not:
heroku config:add JAVA_OPTS='-Xms512M -Xmx1024M -Xss1M
  -XX:+CMSClassUnloadingEnabled
  -XX:MaxPermSize=256M -XX:+UseCompressedOops'

heroku restart

Saving the Run Configuration

If you enable the Share checkbox in the IntelliJ IDEA run configuration dialog box, the definition will be written to .idea/runConfiguration/. Normally you would not check in the contents of .idea/ to your source code repository, but this subdirectory is an exception, and because this run configuration has no local dependencies it can be shared without modification amongst all of your developer team.

$ cat .idea/runConfigurations/Heroku_remote.xml 
<component name="ProjectRunConfigurationManager">
  <configuration default="false" name="Heroku remote" type="Remote" factoryName="Remote">
    <option name="USE_SOCKET_TRANSPORT" value="true" />
    <option name="SERVER_MODE" value="true" />
    <option name="SHMEM_ADDRESS" value="javadebug" />
    <option name="HOST" value="localhost" />
    <option name="PORT" value="9999" />
    <method />
  </configuration>
</component>

Contact Mike Slinn

Unless you are a recruiter, in which case you should not try to make contact!

  • Email
  • Direct: 514-418-0156
  • Mobile: 650-678-2285

Disclaimer

The content on this web site is provided for general information purposes only and does not constitute legal or other professional advice or an opinion of any kind. Users of this web site are advised to seek specific legal advice by contacting their own legal counsel regarding any specific legal issues. Michael Slinn does not warrant or guarantee the quality, accuracy or completeness of any information on this web site. The articles published on this web site are current as of their original date of publication, but should not be relied upon as accurate, timely or fit for any particular purpose.

Accessing or using this web site does not create a client relationship. Although your use of the web site may facilitate access to or communications with Michael Slinn via e-mail or otherwise via the web site, receipt of any such communications or transmissions does not create a client relationship. Michael Slinn does not guarantee the security or confidentiality of any communications made by e-mail or otherwise through this web site.

This web site may contain links to third party web sites. Monitoring the vast information disseminated and accessible through those links is beyond Michael Slinn's resources and he does not attempt to do so. Links are provided for convenience only and Michael Slinn does not endorse the information contained in linked web sites nor guarantee its accuracy, timeliness or fitness for a particular purpose.


comments powered by Disqus

© 1976-2020, Michael Slinn. All rights reserved.