Zobrazují se příspěvky se štítkemjee. Zobrazit všechny příspěvky
Zobrazují se příspěvky se štítkemjee. Zobrazit všechny příspěvky

středa 10. srpna 2022

Asciidoc Cleanup in GlassFish Documentation

 I always feel like Frankenstein when I am doing such things 🤣

When you take a look here on the pull request I created, you will perhaps understand why I did it. 

First I tried to read+search+fix everything one by one, then I tried to use regular expressions in Eclipse, but with some 15 seconds on every change ... I would spend year with that. So after several hours I resigned, time to invent the wheels.

#!/bin/bash
# each line means a set of ids of the same anchor
# the last id is usually the most descriptive and should be used
# the others should be removed
ids=$(grep -h -o -r './' --include=\*.adoc --exclude-dir=target -e '^\(\[\[[0-9A-Za-z_\\-]\+\]\]\)\+' | tr -d "[" | tr -s "]]" ",")

This found all those blocks like this one with labels. I was interested in multiple labels of the same place. Why would someone need three labels plus the implicit one? He didn't. But if some tool generated them, you simply added another. And at that time disks were slow, replacing a label by fulltext search ... eh, damn it, let's create another one.
[[ghmrf]][[GSACG00088]][[osgi-alliance-module-management-subsystem]]


for line in ${ids} ; do
IFS=','
labels=($line); 
unset IFS;
len=${#labels[@]};

  • IFS is a separator.  Don't forget to unset it, because it affects further parsing otherwise.
  • () is an array.
  • length of the array ... don't let me explain this syntax, please ...
So, what we have now:
  • number of labels on the same line
  • if the number is 1, everything is alright
  • if the number is greater, we want to choose one of them (the last one was usually the most descriptive), and get rid of the rest.
  • but how? I can't keep everything in my head, so let's give names to all variables.

if [[ $len != 1 ]]
then
correctId=${labels[$len-1]};
maxIncorrectIdIndex=$(($len-2));

Do you see that evil thing? Bash doesn't subtract 2 from len without braces, ha! It took me a while until I found what to do with that. 

for i in $(seq 0 $maxIncorrectIdIndex) ; do
redundantId=${labels[$i]};
if [[ "$redundantIds" == *",$redundantId,"* ]]; then
echo "Duplicit id must be fixed first: ${redundantId}";
grep -o -r './' --include=\*.adoc --exclude-dir=target -e '^\(\[\[[0-9A-Za-z_\\-]\+\]\]\)\+' | grep "\[${redundantId}\]";
exit 1;
fi
redundantIds="${redundantIds},${redundantId},"

This was quite funny, originally I tried to google some Set implementation for bash, but finally I came to a conclusion that all those implementations are bit overkill. I needed just a string containing all found labels and when I found a redundant label colliding with another redundant label, I forced user, myself, to resolve these conflicts first.

If I would replace them automatically with something else ... it could create invalid xrefs. 

Time for changes. Truth is that these commands could be optimized, but why would I do that? I needed just to pass it once and then commit-push-drink a beer/ice-coffee ... while script went through some 500 files, replacing redundant label usages by the usage of chosen one, and then delete all those declarations.

echo "Replacing $redundantId by $correctId and removing [[$redundantId]] labels...";
find . -type f -name '*.adoc' ! -wholename '*/target/*' -exec sed -i -- "s/#${redundantId}/#${correctId}/g" {} +;  
find . -type f -name '*.adoc' ! -wholename '*/target/*' -exec sed -i -- "s/\[\[${redundantId}\]\]//g" {} +;
done;
fi;
done;

And finally yet one thing ... replace link: by xref: where possible, because then Asciidoctor can validate these references. I found that some types of mistakes still can pass (remember those collisions?), but it is still an awesome tool.

echo "Replacing link references by xref where it is possible.";

find . -type f -name '*.adoc' ! -wholename '*/target/*' -exec sed -i -r -- 's/link:([a-zA-Z0-9\-]+)\.html#/xref:\1.adoc#/g' {} +;

find . -type f -name '*.adoc' ! -wholename '*/target/*' -exec sed -i -r -- 's/link:#/xref:#/g' {} +;

echo "Done.";

Then I started maven clean install, it failed reporting some remaining issues, so I went back to Eclipse and fixed them in an hour. And this is the result in PDF (Okular) and HTML (Opera).



pátek 6. října 2017

REST Web Services with Jackson, Jersey and Payara Micro ... Level II

So ... I will continue with experiments started here and do some real work ... I continued to extend the API introduced in the recent blog, and I encountered some new problems ... for example I could not send a java.util.List in REST response with the same error as I already resolved in the request and also dates were only serialized (have you ever tried to serialize the GregorianCalendar instance?).
So i started to search on StackOverflow and found several solution - but neither one worked or at least it was not easy and nice. Then I started to think how it all works ... ;-)
Now I will stop talking, see the solution. I created new implementation of the web service to be more comprehensible what it does:

Jackson and Jersey

I am so dumb, do you know that? I mixed up these two names so long! So, what is the difference?
  • Jersey is the reference implementation of JAX-RS API. Nothing more to say. THE core. It's home is here
  • Jackson enhances the Jersey with much more stuff to be really usable. It's home is here
And this is a part of pom.xml of the web services war; versions are defined in some parent POM:
    
    <!-- to use ResourceConfig class -->
    <dependency>
      <groupid>org.glassfish.jersey.core</groupid>
      <artifactid>jersey-server</artifactid>
    </dependency>
    <!-- to use JDK8 and other features, like java.time API or automatic ISO time formatting -->
    <dependency>
      <groupid>com.fasterxml.jackson.datatype</groupid>
      <artifactid>jackson-datatype-jdk8</artifactid>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupid>org.apache.logging.log4j</groupid>
      <artifactid>log4j-api</artifactid>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupid>org.apache.logging.log4j</groupid>
      <artifactid>log4j-core</artifactid>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupid>org.apache.commons</groupid>
      <artifactid>commons-lang3</artifactid>
      <scope>compile</scope>
    </dependency>

    <!-- to have better javadocs than in javaee-api -->
    <dependency>
      <groupid>javax.ws.rs</groupid>
      <artifactid>javax.ws.rs-api</artifactid>
      <scope>provided</scope>
    </dependency>    

    <!-- to have all Java EE 7 specifications in one dependency, but to have also the older javadoc -->
    <dependency>
      <groupid>javax</groupid>
      <artifactid>javaee-api</artifactid>
      <scope>provided</scope>
    </dependency>
    

ResouceConfig class

This class should be in root package of all your REST services - see calling of the method named packages in the constructor, that is the magic; it enables the annotation processing. And you know, there is more magic hidden in Maven dependencies not enabled by default. What I didn't know in first blog is that even Jackson is already in Glassfish/Payara (but not everything, see the dependencies again!).

package org.dmatej.experiment.rest;

import javax.ws.rs.ApplicationPath;
import org.glassfish.jersey.server.ResourceConfig;
 

@ApplicationPath("") // use the application root
public class RestAppConfig extends ResourceConfig {
  public RestAppConfig() {
    packages(//
        RestAppConfig.class.getPackage().getName(), // search annotations in this class's package
        "com.fasterxml.jackson"// search in jackson's package
    );
  }
}

ContextResolver interface 

We enabled Jersey and Jackson, but that was not enough! Now we have to enable also the Jackson's extension modules.

package org.dmatej.experiment.rest;

import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
 

@Provider
public class ObjectMapperContextResolver implements ContextResolver {

  private final ObjectMapper MAPPER;

  public ObjectMapperContextResolver() {
    MAPPER = new ObjectMapper();

// This did not find the Jdk8Module
    MAPPER.findAndRegisterModules();
    // enables ISO time parsing and formatting in REST communication.
    MAPPER.registerModule(new Jdk8Module());

    MAPPER.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
  }

  @Override
  public ObjectMapper getContext(Class type) {
    return MAPPER;
  }
}

And now the web service

Now it is pretty trivial and it will do what I expected. No unexplainable errors.

I think this blog entry is big enough and the implementation of Measurement transfer object is trivial, also MeasurementSaveResponse is trivial - it contains list of invalid records and some monitoring data.

The problem was the list - Jersey accepted list as a parameter but had thrown an exception if the list was in the response. Now I don't even need ArrayList in parameters and I can declare an abstract List. It also seems that those transfer objects don't need to have a complete set of getters and setters; Jersey uses reflection.

package org.dmatej.experiment;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import javax.ejb.EJB;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

@Path("measurement")
public class MeasurementRestService {

  private static final Logger LOG = LogManager.getLogger(MeasurementRestService.class);

  // we are stateless, you know ...
  private List refusedMeasurements = new ArrayList<>();

  @POST
  @Path("save/{id1}/{id2}")
  @Consumes(MediaType.APPLICATION_JSON)
  @Produces(MediaType.APPLICATION_JSON)
  public Response save(//
      @PathParam("id1") final String id1, //
      @PathParam("id2") final String id2, //
      final List measurements) {

    final long start = System.currentTimeMillis();
    LOG.trace("save(id1={}, id2={}, measurements={})", id1, id2, measurements);

// invalid elements of the collection are moved to the refusedMeasurements collection.
    process(id1, id2, measurements);

    final MeasurementSaveResponse response = new MeasurementSaveResponse();
    response.setTimeInMillis(System.currentTimeMillis() - start);
    response.setRefusedMeasurements(this.refusedMeasurements);
    final Response restResponse = Response.ok(new GenericEntity<>(response, response.getClass())).build();
    return restResponse;
  }

Payara Micro and Uber jar 

No changes since the first blog. But we did not try it!

Call the web service!


mvn clean install; mvn fish.payara.maven.plugins:payara-micro-maven-plugin:start -pl :experiment-uberjar

... and it will start:

[2017-10-05T22:33:11.850+0200] [] [INFO] [] [javax.enterprise.system.core] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1507235591850] [levelValue: 800] experiment-ws-0.0.1-SNAPSHOT was successfully deployed in 3 118 milliseconds.

[2017-10-05T22:33:11.851+0200] [] [INFO] [] [PayaraMicro] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1507235591851] [levelValue: 800] Deployed 1 archive(s)

[2017-10-05T22:33:11.852+0200] [] [INFO] [] [PayaraMicro] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1507235591852] [levelValue: 800] [[
 
Instance Configuration
Host: localhost
HTTP Port(s): 8080
HTTPS Port(s):
Instance Name: payara-micro
Instance Group: no-cluster
Deployed: experiment-ws-0.0.1-SNAPSHOT ( experiment-ws-0.0.1-SNAPSHOT war /experiment-ws-0.0.1-SNAPSHOT )

]]

[2017-10-05T22:33:11.865+0200] [] [INFO] [] [PayaraMicro] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1507235591865] [levelValue: 800] [[
 
Payara Micro URLs
http://localhost:8080/experiment-ws-0.0.1-SNAPSHOT

'experiment-ws-0.0.1-SNAPSHOT' REST Endpoints
POST    /experiment-ws-0.0.1-SNAPSHOT/measurement/save/{id1}/{id2}

]]

[2017-10-05T22:33:11.865+0200] [] [INFO] [] [PayaraMicro] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1507235591865] [levelValue: 800] Payara Micro  4.1.2.173 #badassmicrofish (build 25) ready in 13 959 (ms)

Then open another console and run several curls:

curl -H "Content-Type: application/json" -X OPTIONS -i http://localhost:8080/experiment-ws-0.0.1-SNAPSHOT/measurement/save/dm790321/xxx;
HTTP/1.1 200 OK
Server: Payara Micro #badassfish
Allow: POST,OPTIONS
Last-modified:  t, 05 X j 2017 11:18:02 CEST
Content-Type: application/vnd.sun.wadl+xml
Date: Thu, 05 Oct 2017 09:18:02 GMT
Content-Length: 715

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<application xmlns="http://wadl.dev.java.net/2009/02">
    <doc xmlns:jersey="http://jersey.java.net/" jersey:generatedBy="Jersey: 2.25.1 2017-01-19 16:23:50"/>
    <grammars/>
    <resources base="http://localhost:8080/experiment-ws-0.0.1-SNAPSHOT/">
        <resource path="measurement/save/dm790321/xxx">
            <method id="save" name="POST">
                <request>
                    <representation mediaType="application/json"/>
                </request>
                <response>
                    <representation mediaType="*/*"/>
                </response>
            </method>
        </resource>
    </resources>
</application>

Another example:

curl -H "Content-Type: application/json" -X GET -i http://localhost:8080/experiment-ws-0.0.1-SNAPSHOT/application.wadl?detail=true
HTTP/1.1 200 OK
Server: Payara Micro #badassfish
Last-modified:  t, 05 X j 2017 15:59:02 CEST
Content-Type: application/vnd.sun.wadl+xml
Date: Thu, 05 Oct 2017 13:59:02 GMT
Content-Length: 5491

<application xmlns="http://wadl.dev.java.net/2009/02">
    <doc jersey:generatedby="Jersey: 2.25.1 2017-01-19 16:23:50" xmlns:jersey="http://jersey.java.net/">
    <doc jersey:hint="This is full WADL including extended resources. To get simplified WADL with users resources only do not use the query parameter detail. Link: http://localhost:8080/experiment-ws-0.0.1-SNAPSHOT/application.wadl" xmlns:jersey="http://jersey.java.net/">
    <grammars>
        <include href="application.wadl/xsd0.xsd">
            <doc title="Generated" xml:lang="en">
        </doc></include>
    </grammars>
    <resources base="http://localhost:8080/experiment-ws-0.0.1-SNAPSHOT/">
        <resource path="measurement">
            <resource path="save/{id1}/{id2}">
                <param name="id1" type="xs:string" xmlns:xs="http://www.w3.org/2001/XMLSchema" />
                <param name="id2" type="xs:string" xmlns:xs="http://www.w3.org/2001/XMLSchema" />
                <method id="save" name="POST">
                    <request>
                        <representation mediatype="application/json">
                    </representation></request>
                    <response>
                        <representation mediatype="application/json">
                    </representation></response>
                </method>
                <method id="apply" name="OPTIONS">
                    <request>
                        <representation mediatype="*/*">
                    </representation></request>
                    <response>
                        <representation mediatype="application/vnd.sun.wadl+xml">
                    </representation></response>
                    <jersey:extended xmlns:jersey="http://jersey.java.net/">true</jersey:extended>
                </method>
                <method id="apply" name="OPTIONS">
                    <request>
                        <representation mediatype="*/*">
                    </representation></request>
                    <response>
                        <representation mediatype="text/plain">
                    </representation></response>
                    <jersey:extended xmlns:jersey="http://jersey.java.net/">true</jersey:extended>
                </method>
                <method id="apply" name="OPTIONS">
                    <request>
                        <representation mediatype="*/*">
                    </representation></request>
                    <response>
                        <representation mediatype="*/*">
                    </representation></response>
                    <jersey:extended xmlns:jersey="http://jersey.java.net/">true</jersey:extended>
                </method>
            </resource>
        </resource>
        <resource path="application.wadl">
            <method id="getWadl" name="GET">
                <response>
                    <representation mediatype="application/vnd.sun.wadl+xml">
                    <representation mediatype="application/xml">
                </representation></representation></response>
                <jersey:extended xmlns:jersey="http://jersey.java.net/">true</jersey:extended>
            </method>
            <method id="apply" name="OPTIONS">
                <request>
                    <representation mediatype="*/*">
                </representation></request>
                <response>
                    <representation mediatype="text/plain">
                </representation></response>
                <jersey:extended xmlns:jersey="http://jersey.java.net/">true</jersey:extended>
            </method>
            <method id="apply" name="OPTIONS">
                <request>
                    <representation mediatype="*/*">
                </representation></request>
                <response>
                    <representation mediatype="*/*">
                </representation></response>
                <jersey:extended xmlns:jersey="http://jersey.java.net/">true</jersey:extended>
            </method>
            <resource path="{path}">
                <param name="path" type="xs:string" xmlns:xs="http://www.w3.org/2001/XMLSchema" />
                <method id="getExternalGrammar" name="GET">
                    <response>
                        <representation mediatype="application/xml">
                    </representation></response>
                    <jersey:extended xmlns:jersey="http://jersey.java.net/">true</jersey:extended>
                </method>
                <method id="apply" name="OPTIONS">
                    <request>
                        <representation mediatype="*/*">
                    </representation></request>
                    <response>
                        <representation mediatype="text/plain">
                    </representation></response>
                    <jersey:extended xmlns:jersey="http://jersey.java.net/">true</jersey:extended>
                </method>
                <method id="apply" name="OPTIONS">
                    <request>
                        <representation mediatype="*/*">
                    </representation></request>
                    <response>
                        <representation mediatype="*/*">
                    </representation></response>
                    <jersey:extended xmlns:jersey="http://jersey.java.net/">true</jersey:extended>
                </method>
                <jersey:extended xmlns:jersey="http://jersey.java.net/">true</jersey:extended>
            </resource>
            <jersey:extended xmlns:jersey="http://jersey.java.net/">true</jersey:extended>
        </resource>
    </resources>
</doc></doc></application>

And finally, let's try timestamp and lists!:

curl -H "Content-Type: application/json" -X POST -d '[{"timestamp":"2017-03-25T15:33:11.000+02:00", "channel":"15", "variable":"XXX", "value":"18"}, {"timestamp":"2017-03-25T15:33:11.000+02:00", "channel":"15", "variable":"XXX", "value":[null]}, {"timestamp":"2017-03-25T15:33:11.000+02:00", "channel":"15", "variable":[null], "value":"18"}]' -i http://localhost:8080/experiment-ws-0.0.1-SNAPSHOT/measurement/save/dm790321/fffff;
HTTP/1.1 200 OK
Server: Payara Micro #badassfish
Content-Type: application/json
Date: Thu, 05 Oct 2017 13:47:05 GMT
Content-Length: 132

{"countOfAccepted":2,"refusedMeasurements":[{"channel":15,"timestamp":"2017-03-25T15:33:11+02:00","value":18.0}],"timeInMillis":244}

úterý 12. září 2017

Experiments with the Payara Micro, level I

New company, new project, new technologies. Okay, I'm experimenting with Payara Micro. Payara team produces more and more examples in several blogs, but they are very trivial and not always usable in production environment. At this time I'm not sure if I would be able to do the evolution to the final professional system, but it is not a problem, because the application modules are simply Java EE standard modules.
The difference is only in final organisation of the modules, deployment and container configuration, so I can create the standard EAR application in parallel to UBER jar with the Payara Micro. So this style of development is perfectly safe.

Target 

Application with the following modules:
  • DAO and bussines logic service module (JPA, JTA, EJB, CDI?), created but nearly empty in this blog 
  • Web service module (JAX-RS), only one simple service method in this blog 
  • GUI module (JSF), not resolved in this blog 
And project will have also following aggregation alternatives:
  • Uber JAR with Payara Micro - experimental, responsive development 
  • EAR for standard Payara domain 
The reason for this separation of modules is that a JSF GUI application obviously have different requirements than a Web service application. It might not be a problem for some time, but it could be a problem later. This is not any premature optimization - this will force developers to keep in mind the separation line between modules and maybe to create some clean API. That will help right now and it will be simplier to split it later.

There may be even more Maven modules:
  • superpom - common Maven plugin configurations, basic dependency management 
  • project parent - aggregator of all project modules 
  • integration tests for the web service module 
  • selenium tests for the gui module

Dead-end streets and good streets 

Well, I had hard two days with the Payara Micro. Blogs helped, but I always needed more and I was always stucked in some weird state. Yes, it was always my fault, but ... okay, now you can learn from my mistakes.

LOG4J2 

I used LOG4J with SLF4J for many years, it was pretty trivial to make it work and a bit harder to grab logs of embedded Payara in integration tests. I have found a memory leak in old LOG4J's reconfiguration and I know perhaps everything about that.
Now it is worthless with LOG4J2. But finally the configuration was also simple despite I still have not found a way to merge logs of the application and Payara. Example log4j2.properties, seems like a good street:
status = info
dest = err
name = PropertiesConfig
#log4j2.debug = true

property.path = target/logs

filter.threshold.type = ThresholdFilter
filter.threshold.level = debug

appender.console.type = Console
appender.console.name = STDOUT
appender.console.layout.type = PatternLayout
appender.console.layout.pattern = %m%n
appender.console.filter.threshold.type = ThresholdFilter
appender.console.filter.threshold.level = info

appender.rolling.type = RollingFile
appender.rolling.name = RollingFile
appender.rolling.fileName = ${path}/experiment-ws.log
appender.rolling.filePattern = ${path}/experiment-ws-%d{MM-dd-yy-HH-mm-ss}-%i.log.gz
appender.rolling.layout.type = PatternLayout
appender.rolling.layout.pattern = %d %p %C{2.} [%t] %m%n
appender.rolling.policies.type = Policies
appender.rolling.policies.time.type = TimeBasedTriggeringPolicy
appender.rolling.policies.time.interval = 2
appender.rolling.policies.time.modulate = true
appender.rolling.policies.size.type = SizeBasedTriggeringPolicy
appender.rolling.policies.size.size=1024MB
appender.rolling.strategy.type = DefaultRolloverStrategy
appender.rolling.strategy.max = 1

logger.ws.name = org.dmatej
logger.ws.level = debug
logger.ws.additivity = false
logger.ws.appenderRef.file.ref = RollingFile

rootLogger.level = debug
rootLogger.appenderRef.stdout.ref = STDOUT
rootLogger.appenderRef.file.ref = RollingFile  

JAX-RS, lists ... Jackson!

Creating the first JAX-RS web service is pretty trivial ... you need two classes, first to configure the context of services in the module, second to implement the service:

import javax.ws.rs.ApplicationPath;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.glassfish.jersey.server.ResourceConfig;

@ApplicationPath("rs")
public class RestAppConfig extends ResourceConfig {

  private static final Logger LOG = LogManager.getLogger(RestAppConfig.class);

  public RestAppConfig() {
    LOG.debug("RestAppConfig()");
    try {
      packages(RestAppConfig.class.getPackage().getName());
      LOG.info("REST configured!");
    } catch (final Exception e) {
      LOG.error("Cannot configure the REST web services!", e);
    }
  }
}
And the second:

import java.util.Arrays;
import java.util.List;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

@Path("user")
public class UserRestService {

  @GET
  @Path("list")
  @Produces(MediaType.APPLICATION_JSON)
  public Response list() {
    final List<String> list = Arrays.asList("Křemílek", "Vochomůrka"); // UTFG ;-)
    return Response.ok(list).build();
  }
} 
Maven command to run the application, it will be used in all following examples:
 mvn clean install;  mvn fish.payara.maven.plugins:payara-micro-maven-plugin:start -pl :experiment-uberjar
Final part of the log:

[2017-09-12T16:31:25.369+0200] [] [INFO] [AS-WEB-GLUE-00172] [javax.enterprise.web] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1505226685369] [levelValue: 800] Loading application [experiment-ws-0.0.1-SNAPSHOT] at [/experiment-ws-0.0.1-SNAPSHOT]

[2017-09-12T16:31:25.681+0200] [] [INFO] [] [javax.enterprise.system.core] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1505226685681] [levelValue: 800] experiment-ws-0.0.1-SNAPSHOT was successfully deployed in 12 472 milliseconds.

[2017-09-12T16:31:25.685+0200] [] [INFO] [] [PayaraMicro] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1505226685685] [levelValue: 800] Deployed 1 archive(s)

[2017-09-12T16:31:25.686+0200] [] [INFO] [] [PayaraMicro] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1505226685686] [levelValue: 800] [[
 
Instance Configuration
Host: dmatej-lenovo
HTTP Port(s): 8080
HTTPS Port(s):
Instance Name: Frail-Barracuda
Instance Group: MicroShoal
Hazelcast Member UUID cbddb9aa-5f21-4ee1-984f-78be942124d4
Deployed: experiment-ws-0.0.1-SNAPSHOT ( experiment-ws-0.0.1-SNAPSHOT war /experiment-ws-0.0.1-SNAPSHOT )

]]

[2017-09-12T16:31:25.702+0200] [] [INFO] [] [PayaraMicro] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1505226685702] [levelValue: 800] [[
 
Payara Micro URLs
http://dmatej-lenovo:8080/experiment-ws-0.0.1-SNAPSHOT

'experiment-ws-0.0.1-SNAPSHOT' REST Endpoints
GET     /experiment-ws-0.0.1-SNAPSHOT/rs/user/list

]]
Ok, that was pretty simple. But the service does not work and ends up with HTTP 500 (try it with browser):

[2017-09-12T20:19:02.013+0200] [] [SEVERE] [] [org.glassfish.jersey.message.internal.WriterInterceptorExecutor] [tid: _ThreadID=22 _ThreadName=http-thread-pool::http-listener(3)] [timeMillis: 1505240342013] [levelValue: 1000] MessageBodyWriter not found for media type=application/json, type=class java.util.Arrays$ArrayList, genericType=class java.util.Arrays$ArrayList.

Solution 

Only add the dependency on Jackson to pom.xml and the following line as the first to the RestAppConfig's constructor:
register(JacksonFeature.class);
The final pom.xml of the ws module:

 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <artifactId>experiment-ws</artifactId>
  <packaging>war</packaging>
  <name>Experiment - Web Services</name>
  <parent>
    <groupId>org.dmatej.experiment</groupId>
    <artifactId>parent-pom</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <relativePath>..</relativePath>
  </parent>
  <dependencies>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-api</artifactId>
    </dependency>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-core</artifactId>
    </dependency>
    <dependency>
      <groupId>javax.ws.rs</groupId>
      <artifactId>javax.ws.rs-api</artifactId>
    </dependency>
    <dependency>
      <groupId>javax</groupId>
      <artifactId>javaee-api</artifactId>
    </dependency>
    <!-- to use optimized JAX-RS configuration -->
    <dependency>
      <groupId>org.glassfish.jersey.core</groupId>
      <artifactId>jersey-server</artifactId>
    </dependency>
    <!-- to simply use lists in JSONs -->
    <dependency>
      <groupId>org.glassfish.jersey.media</groupId>
      <artifactId>jersey-media-json-jackson</artifactId>
    </dependency>
  </dependencies>
  <build>
    <resources>
      <resource>
        <directory>src/main/resources</directory>
        <filtering>false</filtering>
        <includes>
          <include>**/*.properties</include>
        </includes>
      </resource>
    </resources>
  </build>
</project>

JDBC Pool 

This was a real pain but solution was so simple ... do you know what I hate? NullpointerException. In fact I appreciate that it exists, because it tells "the programmer was not careful". And if the programmer was not careful, it is a bug - maybe even trivial to fix. But another exception I have seen was ClassNotFoundException ... This is the pom.xml of the UBER jar:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>org.dmatej.experiment</groupId>
  <artifactId>experiment-uberjar</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>Experiment Uberjar</name>
  <parent>
    <groupId>org.dmatej.experiment</groupId>
    <artifactId>superpom</artifactId>
    <version>0.0.1-SNAPSHOT</version>
  </parent>
  <build>
    <resources>
      <resource>
        <directory>src/main/resources</directory>
        <filtering>false</filtering>
        <includes>
          <include>**/*.txt</include>
        </includes>
      </resource>
    </resources>
    <plugins>
      <!-- HOWTO: https://github.com/payara/maven-plugins -->
      <plugin>
        <groupId>fish.payara.maven.plugins</groupId>
        <artifactId>payara-micro-maven-plugin</artifactId>
        <version>1.0.0</version>
        <executions>
          <execution>
            <goals>
              <goal>bundle</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <payaraVersion>4.1.2.173</payaraVersion>
          <useUberJar>true</useUberJar>
          <deployArtifacts>
            <deployArtifact>
              <groupId>org.dmatej.experiment</groupId>
              <artifactId>experiment-ws</artifactId>
              <version>${project.version}</version>
              <type>war</type>
            </deployArtifact>
          </deployArtifacts>
          <customJars>
            <customJar>
              <groupId>org.apache.logging.log4j</groupId>
              <artifactId>log4j-api</artifactId>
            </customJar>
            <customJar>
              <groupId>org.apache.logging.log4j</groupId>
              <artifactId>log4j-core</artifactId>
            </customJar>
            <customJar>
              <groupId>mysql</groupId>
              <artifactId>mysql-connector-java</artifactId>
            </customJar>
          </customJars>
          <commandLineOptions>
            <commandLineOption>
              <key>--autobindhttp</key>
              <value>true</value>
            </commandLineOption>
            <commandLineOption>
              <key>--prebootcommandfile</key>
              <value>${project.build.outputDirectory}/prepare-resources.txt</value>
            </commandLineOption>
          </commandLineOptions>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>
The prepare-resources.txt was this (note that empty lines are interpreted as an error):

set configs.config.server-config.admin-service.das-config.dynamic-reload-enabled=false
set configs.config.server-config.admin-service.das-config.autodeploy-enabled=false
create-jdbc-connection-pool --datasourceclassname com.mysql.cj.jdbc.MysqlDataSource --restype javax.sql.DataSource --property user=test:password=test:DatabaseName=experiment_test:ServerName=localhost:port=3306:zeroDateTimeBehavior=convertToNull:useUnicode=true:useJDBCCompliantTimezoneShift=true:useLegacyDatetimeCode=true:serverTimezone=UTC:characterEncoding=UTF-8 experiment-mysql
set resources.jdbc-connection-pool.experiment-mysql.steady-pool-size=5
set resources.jdbc-connection-pool.experiment-mysql.max-pool-size=20
set resources.jdbc-connection-pool.experiment-mysql.connection-validation-method=auto-commit
set resources.jdbc-connection-pool.experiment-mysql.is-connection-validation-required=true
set resources.jdbc-connection-pool.experiment-mysql.fail-all-connections=true
ping-connection-pool experiment-mysql
But it did not work, server startup failed and I had no idea why ... and Payara did not help me ... there were two kind of stacktraces:

java.lang.RuntimeException: Orb initialization erorr
        at org.glassfish.enterprise.iiop.api.GlassFishORBHelper.getORB(GlassFishORBHelper.java:191)
        at com.sun.enterprise.naming.impl.SerialContext.getORB(SerialContext.java:349)
        at com.sun.enterprise.naming.impl.SerialContext.getProviderCacheKey(SerialContext.java:356)
        at com.sun.enterprise.naming.impl.SerialContext.getRemoteProvider(SerialContext.java:386)
        at com.sun.enterprise.naming.impl.SerialContext.getProvider(SerialContext.java:331)
        at com.sun.enterprise.naming.impl.SerialContext.lookup(SerialContext.java:480)
        at com.sun.enterprise.naming.impl.SerialContext.lookup(SerialContext.java:440)
        at javax.naming.InitialContext.lookup(InitialContext.java:417)
        at org.glassfish.resourcebase.resources.naming.ResourceNamingService.lookup(ResourceNamingService.java:236)
        at com.sun.enterprise.connectors.service.ConnectorConnectionPoolAdminServiceImpl.getConnectorConnectionPool(ConnectorConnectionPoolAdminServiceImpl.java:799)
        at com.sun.enterprise.connectors.service.ConnectorConnectionPoolAdminServiceImpl.obtainManagedConnectionFactory(ConnectorConnectionPoolAdminServiceImpl.java:938)
        at com.sun.enterprise.connectors.service.ConnectorConnectionPoolAdminServiceImpl.getUnpooledConnection(ConnectorConnectionPoolAdminServiceImpl.java:549)
        at com.sun.enterprise.connectors.service.ConnectorConnectionPoolAdminServiceImpl.testConnectionPool(ConnectorConnectionPoolAdminServiceImpl.java:430)
        at com.sun.enterprise.connectors.ConnectorRuntime.pingConnectionPool(ConnectorRuntime.java:1162)
        at org.glassfish.connectors.admin.cli.PingConnectionPool.execute(PingConnectionPool.java:143)
        at com.sun.enterprise.v3.admin.CommandRunnerImpl$2$1.run(CommandRunnerImpl.java:544)
        at com.sun.enterprise.v3.admin.CommandRunnerImpl$2$1.run(CommandRunnerImpl.java:540)
        at java.security.AccessController.doPrivileged(Native Method)
        at javax.security.auth.Subject.doAs(Subject.java:360)
        at com.sun.enterprise.v3.admin.CommandRunnerImpl$2.execute(CommandRunnerImpl.java:539)
        at com.sun.enterprise.v3.admin.CommandRunnerImpl$3.run(CommandRunnerImpl.java:570)
        at com.sun.enterprise.v3.admin.CommandRunnerImpl$3.run(CommandRunnerImpl.java:562)
        at java.security.AccessController.doPrivileged(Native Method)
        at javax.security.auth.Subject.doAs(Subject.java:360)
        at com.sun.enterprise.v3.admin.CommandRunnerImpl.doCommand(CommandRunnerImpl.java:561)
        at com.sun.enterprise.v3.admin.CommandRunnerImpl.doCommand(CommandRunnerImpl.java:1469)
        at com.sun.enterprise.v3.admin.CommandRunnerImpl.access$1300(CommandRunnerImpl.java:111)
        at com.sun.enterprise.v3.admin.CommandRunnerImpl$ExecutionContext.execute(CommandRunnerImpl.java:1851)
        at com.sun.enterprise.v3.admin.CommandRunnerImpl$ExecutionContext.execute(CommandRunnerImpl.java:1727)
        at com.sun.enterprise.admin.cli.embeddable.CommandExecutorImpl.executeCommand(CommandExecutorImpl.java:169)
        at com.sun.enterprise.admin.cli.embeddable.CommandExecutorImpl.run(CommandExecutorImpl.java:94)
        at fish.payara.micro.boot.runtime.BootCommand.execute(BootCommand.java:65)
        at fish.payara.micro.boot.runtime.BootCommands.executeCommands(BootCommands.java:105)
        at fish.payara.micro.boot.runtime.BootCommands.executeCommands(BootCommands.java:99)
        at fish.payara.micro.impl.PayaraMicroImpl.bootStrap(PayaraMicroImpl.java:987)
        at fish.payara.micro.impl.PayaraMicroImpl.main(PayaraMicroImpl.java:186)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at fish.payara.micro.boot.loader.MainMethodRunner.run(MainMethodRunner.java:48)
        at fish.payara.micro.boot.loader.Launcher.launch(Launcher.java:107)
        at fish.payara.micro.boot.loader.Launcher.launch(Launcher.java:70)
        at fish.payara.micro.boot.PayaraMicroLauncher.main(PayaraMicroLauncher.java:79)
        at fish.payara.micro.PayaraMicro.main(PayaraMicro.java:361)
Caused by: java.lang.NullPointerException
        at org.glassfish.enterprise.iiop.api.GlassFishORBHelper.getORB(GlassFishORBHelper.java:163)
        ... 44 more
[2017-09-12T21:42:28.782+0200] [] [SEVERE] [] [javax.enterprise.resource.resourceadapter.com.sun.enterprise.connectors] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1505245348782] [levelValue: 1000] RAR6001 : Class Not found : com.sun.gjc.spi.ResourceAdapterImpl


com.sun.appserv.connectors.internal.api.ConnectorRuntimeException: Error in creating active RAR
        at com.sun.enterprise.connectors.ActiveRAFactory.createActiveResourceAdapter(ActiveRAFactory.java:111)
        at com.sun.enterprise.connectors.service.ResourceAdapterAdminServiceImpl.createActiveResourceAdapter(ResourceAdapterAdminServiceImpl.java:212)
        at com.sun.enterprise.connectors.service.ResourceAdapterAdminServiceImpl.createActiveResourceAdapter(ResourceAdapterAdminServiceImpl.java:348)
        at com.sun.enterprise.connectors.ConnectorRuntime.createActiveResourceAdapter(ConnectorRuntime.java:405)
        at com.sun.enterprise.connectors.service.ConnectorService.loadDeferredResourceAdapter(ConnectorService.java:184)
        at com.sun.enterprise.connectors.service.ConnectorService.loadResourcesAndItsRar(ConnectorService.java:148)
        at com.sun.enterprise.connectors.service.ConnectorService.checkAndLoadPool(ConnectorService.java:325)
        at com.sun.enterprise.connectors.service.ConnectorConnectionPoolAdminServiceImpl.getUnpooledConnection(ConnectorConnectionPoolAdminServiceImpl.java:553)
        at com.sun.enterprise.connectors.service.ConnectorConnectionPoolAdminServiceImpl.testConnectionPool(ConnectorConnectionPoolAdminServiceImpl.java:430)
        at com.sun.enterprise.connectors.ConnectorRuntime.pingConnectionPool(ConnectorRuntime.java:1162)
        at org.glassfish.connectors.admin.cli.PingConnectionPool.execute(PingConnectionPool.java:143)
        at com.sun.enterprise.v3.admin.CommandRunnerImpl$2$1.run(CommandRunnerImpl.java:544)
        at com.sun.enterprise.v3.admin.CommandRunnerImpl$2$1.run(CommandRunnerImpl.java:540)
        at java.security.AccessController.doPrivileged(Native Method)
        at javax.security.auth.Subject.doAs(Subject.java:360)
        at com.sun.enterprise.v3.admin.CommandRunnerImpl$2.execute(CommandRunnerImpl.java:539)
        at com.sun.enterprise.v3.admin.CommandRunnerImpl$3.run(CommandRunnerImpl.java:570)
        at com.sun.enterprise.v3.admin.CommandRunnerImpl$3.run(CommandRunnerImpl.java:562)
        at java.security.AccessController.doPrivileged(Native Method)
        at javax.security.auth.Subject.doAs(Subject.java:360)
        at com.sun.enterprise.v3.admin.CommandRunnerImpl.doCommand(CommandRunnerImpl.java:561)
        at com.sun.enterprise.v3.admin.CommandRunnerImpl.doCommand(CommandRunnerImpl.java:1469)
        at com.sun.enterprise.v3.admin.CommandRunnerImpl.access$1300(CommandRunnerImpl.java:111)
        at com.sun.enterprise.v3.admin.CommandRunnerImpl$ExecutionContext.execute(CommandRunnerImpl.java:1851)
        at com.sun.enterprise.v3.admin.CommandRunnerImpl$ExecutionContext.execute(CommandRunnerImpl.java:1727)
        at com.sun.enterprise.admin.cli.embeddable.CommandExecutorImpl.executeCommand(CommandExecutorImpl.java:169)
        at com.sun.enterprise.admin.cli.embeddable.CommandExecutorImpl.run(CommandExecutorImpl.java:94)
        at fish.payara.micro.boot.runtime.BootCommand.execute(BootCommand.java:65)
        at fish.payara.micro.boot.runtime.BootCommands.executeCommands(BootCommands.java:105)
        at fish.payara.micro.boot.runtime.BootCommands.executeCommands(BootCommands.java:99)
        at fish.payara.micro.impl.PayaraMicroImpl.bootStrap(PayaraMicroImpl.java:987)
        at fish.payara.micro.impl.PayaraMicroImpl.main(PayaraMicroImpl.java:186)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at fish.payara.micro.boot.loader.MainMethodRunner.run(MainMethodRunner.java:48)
        at fish.payara.micro.boot.loader.Launcher.launch(Launcher.java:107)
        at fish.payara.micro.boot.loader.Launcher.launch(Launcher.java:70)
        at fish.payara.micro.boot.PayaraMicroLauncher.main(PayaraMicroLauncher.java:79)
        at fish.payara.micro.PayaraMicro.main(PayaraMicro.java:361)
Caused by: java.lang.ClassNotFoundException: com.sun.gjc.spi.ResourceAdapterImpl
        at com.sun.enterprise.v3.server.APIClassLoaderServiceImpl$APIClassLoader.loadClass(APIClassLoaderServiceImpl.java:245)
        at com.sun.enterprise.v3.server.APIClassLoaderServiceImpl$APIClassLoader.loadClass(APIClassLoaderServiceImpl.java:237)
        at com.sun.enterprise.connectors.ActiveRAFactory.createActiveResourceAdapter(ActiveRAFactory.java:103)
        ... 40 more
]]

Dead-end street: comment out ping 

Okay, stacktraces are gone. But I don't know if the pool has been created and if it works.

Dead-end street: add dependencies 

Idea: add missing dependencies. The Payara versions are not in Maven Central, so I tried to add glassfish versions of org.glassfish.main.jdbc.jdbc-ra.jdbc-core:jdbc-core:4.1.2 and org.glassfish.main.jdbc.jdbc-ra.jdbc40:jdbc40:4.1.2 ... Result? Several warnings like this and finally exception. Dumb idea? Something similar helped us with old versions of the Embedded Payara started by JUnit integration tests but here it was only a cargo antipattern.

[2017-09-12T21:36:37.619+0200] [] [WARNING] [] [javax.enterprise.resource.resourceadapter.com.sun.enterprise.connectors.util] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1505244997619] [levelValue: 900] RAR8000 : The method setLogJdbcCalls is not present in the class : com.sun.gjc.spi.DSManagedConnectionFactory

com.sun.appserv.connectors.internal.api.ConnectorRuntimeException: Failed to create MCF for experiment-mysql
        at com.sun.enterprise.connectors.service.ConnectorConnectionPoolAdminServiceImpl.createConnectorConnectionPool(ConnectorConnectionPoolAdminServiceImpl.java:195)
        at com.sun.enterprise.connectors.ConnectorRuntime.createConnectorConnectionPool(ConnectorRuntime.java:331)
        at org.glassfish.jdbc.deployer.JdbcConnectionPoolDeployer.actualDeployResource(JdbcConnectionPoolDeployer.java:201)
        at org.glassfish.jdbc.deployer.JdbcConnectionPoolDeployer.deployResource(JdbcConnectionPoolDeployer.java:170)
        at com.sun.enterprise.connectors.service.ConnectorService.loadDeferredResources(ConnectorService.java:233)
        at com.sun.enterprise.connectors.service.ConnectorService$1.run(ConnectorService.java:153)
        at java.security.AccessController.doPrivileged(Native Method)
Those two dependencies now remove, they are not needed and more, they are not compatible.

Look into the Payara sources and think

 ... and create this issue: https://github.com/payara/Payara/issues/1967 Oh, by the way, why I did not think it before? ORB factory is simply initialized after the prebootcommandfile execution! Ok, let's move the ping to a new file postboot.txt and add another commandLineOption to pom.xml:
<commandLineOption>
  <key>--postbootcommandfile</key>
  <value>${project.build.outputDirectory}/postboot.txt</value>
</commandLineOption>

MySQL and time zones 

Pool ping was still failing with some weird error message about unknown CEST timezone. StackOverflow advices did not work, neither one about configuring the JDBC driver. I have found several bugs reported to MySQL devs: https://bugs.mysql.com/bug.php?id=86425
I tried to change the server's default-time-zone via the MySql Workbench with no success until I noted that it updates incorrect file in my user's home directory. Finally I added these lines into /etc/mysql/my.cnf and restarted the mysql service ... and it worked.
[mysqld]
default-time-zone = +00:00

Success 

Yes, that was all. But ... there is nothing interesting in the log output, no logging about asadmin commands, no logging about their success. I was lazy to create my own logging.properties and to add path as another commandLineOption so I hacked the jar in maven repository (please, don't do this, don't be lazy!) ... the nearest usable loggers and logs was these:

[2017-09-12T22:23:53.622+0200] [] [FINE] [] [javax.enterprise.resource.resourceadapter.org.glassfish.jdbcruntime] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1505247833622] [levelValue: 500] [CLASSNAME: org.glassfish.jdbcruntime.JdbcRuntimeExtension] [METHODNAME: isConnectionPoolReferredInServerInstance] JDBC resource jdbc/experiment-jta refers experiment-mysql in this server instance and is enabled
 

[2017-09-12T22:23:54.398+0200] [] [FINE] [] [org.glassfish.naming] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1505247834398] [levelValue: 500] [CLASSNAME: com.sun.enterprise.naming.util.NamingUtilsImpl] [METHODNAME: makeCopyOfObject] [[
  ** makeCopyOfObject:: ConnectorConnectionPool :: experiment-mysql
steady size: 5
max pool size: 20
max wait time: 60000
pool resize qty: 2
Idle timeout: 300
failAllConnections: true
Transaction Support Level: 1
isConnectionValidationRequired_ true
preferValidateOverRecreate_ false
matchConnections_ false
associateWithThread_ false
lazyConnectionAssoc_ false
lazyConnectionEnlist_ false
maxConnectionUsage_ 0
pingPoolDuringCreation_ false
poolingOn_ true
validateAtmostOncePeriod_ 0
connectionLeakTracingTimeout_0
connectionReclaim_false
connectionCreationRetryAttempts_0
connectionCreationRetryIntervalInMilliSeconds_10
nonTransactional_ false
nonComponent_ false
ConnectorDescriptorInfo ->
rarName: __ds_jdbc_ra
resource adapter class: com.sun.gjc.spi.ResourceAdapterImpl
connection def name: javax.sql.DataSource
MCF Config properties-> ClassName:com.mysql.cj.jdbc.MysqlDataSource
ConnectionValidationRequired:true
ValidationMethod:auto-commit
ValidationTableName:
ValidationClassName:
TransactionIsolation:
GuaranteeIsolationLevel:true
StatementWrapping:true
LogJdbcCalls:false
SlowQueryThresholdInSeconds:-1
StatementTimeout:-1
PoolMonitoringSubTreeRoot:resources/experiment-mysql
PoolName:experiment-mysql
StatementCacheSize:0
StatementCacheType:
InitSql:null
SqlTraceListeners:fish.payara.jdbc.SilentSqlTraceListener
StatementLeakTimeoutInSeconds:0
StatementLeakReclaim:false
DatabaseName:iqhouse_experiment
User:test
Password:****
ServerName:localhost
DriverProperties:setcharacterEncoding#UTF-8##setport#3306##setuseJDBCCompliantTimezoneShift#true##setuseLegacyDatetimeCode#true##setuseUnicode#true##setserverTimezone#UTC##setzeroDateTimeBehavior#convertToNull##
Delimiter:#
EscapeCharacter:\
]]  
Now I can continue to level 2: Good REST services ... to be continued ;)

neděle 3. července 2016

Javisté a databáze - základní chyby

Na školení jsem všem implicitně vynadal, že nikdo pořádně neumí používat ani JDBC, ale chtějí (dobře) používat JPA nebo JTA. A co teprve, aby rozuměli trochu tomu, co jejich zacházení způsobí na databázovém serveru ...
Pak se diví, že jim to celé padá na hubu - typicky začnou optimalizovat SQL, ale marně. Jen málokoho napadne, že databázi dávají kapky - pro každý váš požadavek databáze alokuje nějaké zdroje v operační paměti, přidělí vlákno, příkaz zabere nějaké místo na file systému, a databáze čeká až na signál od vás, že už jste hotoví.
Tož si překontrolujte, jestli si databázi nepřetěžujete ...
  • zavíráním statementů
    • kolikrát za sebou generujete stejný prepared statement?
    • nezavíráním statementů
    • jak dlouho má db držet statement?
    • kolik stejných prepared statementů držíte najednou?
    • poslední pomoc: omezit počet použití konexe z poolu, pak jí AS nuceně zavře a otevře novou (Payara)
  • nezavíráním result setů
    • jak dlouho má db držet nalezená data, kurzor?
    • kolik dat to je?
    • fetch size
  • zbytečně dlouhými transakcemi se spoustou změn
    • víte, co se děje v databázi?
    • víte, že i zdroje databáze jsou omezené?
  • nemazáním dočasných tabulek, vytvořených v poolovaných konexích
    • je to v podstatě leak, zapomenutá data pak můžou být náhodně nalezena jiným dotazem, čili hypoteticky to může být i bezpečnostní díra.
  • nadměrným využíváním indexů nebo naopak jejich absencí
    • každé vložení dat způsobí změnu v indexech
    • databáze jsou různě chytré/hloupé při optimalizacích
    • indexy pomáhají při čtení
    • indexy škodí při zápisu
    • někdy se hodí replikace db tak, aby db pro vyhledávání byla důsledněji indexovaná než ta pro zápis
  • chybným nastavením JDBC poolu
    • default lock timeout
    • velikosti bufferů
    • různé optimalizace
    • rozdělení datových zdrojů podle použití (exporty vs. CRUD)
  • chybným nastavením:
    • stmt.setQueryTimeout
    • stmt.setMaxRows
    • stmt.setFetchSize
    • conn.setTransactionIsolation
    • conn.setResultSetConcurrency
    • conn.setHoldability
JPA samozřejmě k relační db přistupují přes jdbc, ale pozor!
  • JDBC transakce není nutně pod JPA transakcí a už vůbec nejsou svázané 1:1
  • JPA transakce není JTA transakce
  • můžou. Je to záležitost nastavení "persistence unit" a také nastavení hintů (viz Query.setHint)
  • EntityManager.unwrap(Connection.class) znamená spuštění JDBC transakce a svázání 1:1 JDBC konexe a JPA manažera se vším, co to obnáší.
JTA se pak řídí hlavně anotacemi, ale pozor pozor!!!
  • pokud si nedáte pozor, můžete si připravit neskutečné magické transakční peklo
  • „requires“ je dobrý default
  • „requires new“ se někdy hodí, ale … uvědomte si, co to znamená!
  • některé posloupnosti těchto hodnot dokážou naprosto šílené věci – s kolika JPA kontexty a JDBC konexemi nakonec opravdu pracujete?
Jste programátoři, vědci, ne pistolníci z televizního divokého západu. Na riskování není čas, musíte přesně vědět, co děláte ;-)

čtvrtek 10. prosince 2015

Loadbalancing with Payara cluster and Apache2

I have tested the configuration on two operating systems - CentOS 7 and Kubuntu 15.10. These instructions are for CentOS, because there it is a bit more complicated.

All application server instances run on Payara 4.1.1.154 - two on CentOS 7 and one on Solaris5.10. Note that for correct cluster replication you need to have configured the multicast routing - or to have all instances on the same network ;)

1) You need to do this as root:
# aka apache2 and modules in debian linuxes
yum install httpd
2) You need time synchronization - one minute error is fatal. You can select another server for time sychronization - check also if it is accessible from the server.
yum install ntp ntpdate
chkconfig ntpd on
ntpdate pool.ntp.org
3) Edit the httpd.conf file and add two lines (use correct hostname, valid from the outside world):
vim /etc/httpd/conf/httpd.conf
ServerName myhost.mydomain.org
LoadModule rewrite_module modules/mod_rewrite.so

4) Edit httpd configuration file and put the loadbalancer settings in it. 
vim /etc/httpd/conf.d/00-default.conf
  • VirtualHost will run on port 80
  • you need some time to hold the session on one instance, where the user logs in - in this time the new session will be replicated to other payara instances in cluster. If the next request would be faster, the session would be invalidated. This is the reason why you need also the ROUTEID cookie - the stickysession changes it's value because a new session is created.
  • third internal host has status +H - that means "hot standby" - it will be routed only if other hosts would be inaccessible.
<VirtualHost *:80>
    ProxyRequests Off
    RewriteEngine Off
    Header add Set-Cookie "ROUTEID=.%{BALANCER_WORKER_ROUTE}e; path=/; Max-Age=60;" env=BALANCER_ROUTE_CHANGED
    ServerName myhost.mydomain.org
    LogLevel debug
    ProxyPass /balancer-manager !
    ProxyPass / balancer://myhost.mydomain.org/ stickysession=ROUTEID
    ProxyPassReverse /  http://myinternal1.mydomain:8300/
    ProxyPassReverse /  http://myinternal2.mydomain:8300/
    ProxyPassReverse /  http://myinternal3.mydomain:8300/
    ProxyPassReverseCookieDomain myinternal1.mydomain myhost.mydomain.org
    ProxyPassReverseCookieDomain myinternal2.mydomain myhost.mydomain.org
    ProxyPassReverseCookieDomain myinternal3.mydomain myhost.mydomain.org
    ProxyPassReverseCookiePath / /
    <Proxy balancer://cisas1.lab.i.cz>
        BalancerMember http://myinternal1.mydomain:8300 loadfactor=1 route=int1
        BalancerMember http://myinternal2.mydomain:8300 loadfactor=1 route=int2
        BalancerMember http://myinternal3.mydomain:8300 status=+H route=int3
        Order Deny,Allow
        Deny from none
        Allow from all
        ProxySet stickysession=ROUTEID
        ProxySet lbmethod=bytraffic
    </Proxy>
</VirtualHost>
5) Allow the httpd service to access the outside world via TCP - without that you will get only HTTP 503 and some message in error_log that the action was declined.
/usr/sbin/setsebool httpd_can_network_connect 1
6) Run the httpd service (and loadbalancing) and watch logs
systemctl restart httpd.service
journalctl -xn
tail -1000f /var/log/httpd/error_log

Payara, aneb zmrtvýchvstání aplikačního serveru Glassfish

SunOne7

Glassfish má za sebou dlouhatánskou cestu ... první, s čím jsem přišel do styku, byl SunOne7. Ten vznikl jako výsledek spolupráce s Netscape a Oracle stále ještě udržuje jakousi stařičkou dokumentaci.

Jeho výhodou byla jedna z prvních podpor EJB (což se ukázalo i nevýhodou, jelikož tato technologie, tj. EJB2 byla prakticky nepoužitelná) a administrační GUI, které naopak použitelné bylo. Z něj se daly konfigurovat a spouštět i vypínat instance aplikačního serveru, čili na tu dobu šlo o vskutku "enterprise" řešení, které v té době nemělo obdoby.
Výhod bylo víc, například integrace JMS a dalších J2EE technologií, aktuálních v roce 2003.
Největší výhodou byla ovšem výborná stabilita. Tenhle aplikáč nám snad nikdy nespadnul, pokud nebudu počítat memory leaky v našich vlastních aplikacích.

Mezi nevýhody patřilo využívání nativních knihoven a tudíž možnost spuštění pouze na OS Solaris, Windows a RedHat 7.2. Rozchodit monstrum jinde vyžadovalo náhodné vyměňování nativních knihoven, než se administrátor "trefil" do konstelace, která fungovala.
A jedna nevýhoda fatální - zapomeňte na zdrojáky nebo artefakty ve veřejných úložištích.
No, pryč od toho ... užili jsme si dost srandy :-)

Sun Java Application Server 8

V roce 2005 přišly specifikace JEE5, JDK5, EJB3, JPA a další, díky kterým se stal starý aplikáč v podstatě mrtvou technologií. Sun se do toho vrhnul po hlavě, část implementací integroval od jiných autorů, část napsal sám - práce odvedl mnoho a velmi rychle, byť s diskutabilní kvalitou.

Tou dobou se už začínala probouzet konkurence - z největších aktuálních jmenujme WebSphere, WebLogic, TomEE, WildFly/JBoss, Jetty, a další. Mírně zastarávající seznamy s malým srovnáním viz wiki nebo wiki.

Přiznávám, že s touto verzí jsem neměl moc zkušeností, jen si pamatuji, že to byl takový mezičlánek, ve kterém byla spousta nedodělků a chyb. V principu to fungovalo, ale ...

Sun Java Application Server 9 aneb Sun Glassfish Enterprise Server 2

SJAS8 byl v podstatě prototyp. SJAS9, později přejmenován na SGES2, znamenal dotažení technologií do konce. Velké problémy byly s JMS (OpenMQ), což byl server provozovaný v serveru, při chybné konfiguraci se prostě zasekl. Důvody jste museli hledat mimo Glassfish, přičemž trvalo dlouho, než jste přišli na to, v čem je přesně problém.
Komunikace mezi GF a MQ probíhala přes porty, a variant konfigurací bylo mnoho, snad ani autorům původně nedošlo, jakou variabilitu jim to dává. To je ale námět spíše na jiný blog - stručně - triviální varianta je "embedded", resp. "zapouzdřený" server, složitá a škálovatelná varianta je "remote" server ...

SJAS9 přišel v roce 2007, zkraje se ještě stávalo, že "zůstal viset" právě díky JMS, později ale byly i tyto chyby vyřešeny a poslední verze SGES2.1.1 byla velmi stabilní a bezproblémová (pokud jí nedestabilizovaly vaše chyby ve vašich aplikacích ;-) ).

Hlavní výhody proti dřívějším verzím:
  • podpora clusteringu - bylo možné provozovat n instancí na m strojích, mezi nimiž se replikovaly sessions uživatelů a bylo možné např. postupné nasazení nové verze, aniž by došlo k výpadku (to samozřejmě mělo svá pravidla, o tom zase jindy).
  • žádné nativní knihovny - kde běží JDK5/6, běží Glassfish (jen občas narazíte na problémy např. s Windows).
  • propracovaná dokumentace
  • kompletní podpora JEE5

Glassfish3

V roce 2010 Oracle koupil Sun a všechno se změnilo. Již předtím jaksi "v ilegalitě" vznikl tzv. Embedded Glassfish, na který jsem narazil prakticky náhodou - kupodivu ve veřejných úložištích, a začal ho používat k integračním testům. Ano, stačil mi JUnit, abych mohl testovat EJB3 s JMS, JPA a se vším všudy!
Embedded Glassfish je totiž přebalené vydání Glassfishe3 do jediného jar souboru. Chybí mu pár XML, která se dají snadno dotáhnout z jiných závislostí a jedete - tak málo stačilo k odstranění největší výtky vůči "enterprise aplikačním serverům"!

Glassfish3 náš tým nakonec přeskočil, ale pomohl nám díky zpětné kompatibilitě se SGES2 právě s testováním.
Embedded Glassfish byl nakonec opět pohlcen Glassfishem a stal se součástí jeho buildu a repository.

V roce 2011 Oracle oficiálně vydal Glassfish3 jako referenční implementaci JEE6. Vzápětí se ale probral JBoss a prakticky Glassfish3 převálcoval.

Byrokracie Oracle navíc silně rozladila některé vývojáře a byla poslední kapkou k jejich přestupu - kam jinam - často k JBoss.
Po vyjádření Oracle o ukončení komerční podpory Glassfishe a o vyčlenění "clusteringu" pouze do komerční verze to vypadalo, že Glassfish prostě skončí, zemře, je konec. Jenže se ukázalo, že tak snadné to mít Oracle nebude ...

Glassfish4 - smrt ... NEBUDE!

Oracle totiž narazil na pár háčků. Nápad s clusteringem vyprovokoval poměrně hlučnou odezvu vývojářů i mimo Oracle, takže od něj bylo ustoupeno. Nápad s ukončením komerční podpory sice Oracle dotáhl do konce, ale nikoho to zase tak moc netrápilo, protože i má osobní zkušenost s komeční podporou Sun/Oracle byla velmi špatná (a je doposud).
Licence Glassfishe ale umožňovala alespoň pasivní přístup ke zdrojákům, a bylo otázkou času, kdy dojde k tomu, kdy je někdo vyžene na GitHub (ano, byl jsem to já, ale ukázalo se, že nejsem první, viz níže :D).

Druhý háček spočíval v tom, že komunita JEE stále potřebuje referenční implementaci a nikdo si jí nemohl jen tak přivlastnit a zamezit přístupu k ní ostatním.

No a poslední háček - všechny implementace JEE7 používají komponenty svých konkurentů. Weld vyvíjí JBoss. EclipseLink vyvíjí Eclipse+Oracle. Catalinu vyvíjí Apache. Pokud by Oracle "zařízl" Glassfish, patrně by nevydal už víc jak jednu jedinou verzi WebLogic, vývoj by se zbrzdil.

A tak v roce 2013 Oracle vydal Glassfish4 jako referenční implementaci JEE7. Změn nebylo mnoho oproti GF3, spoustu práce odvedli externí programátoři, Oracle se zjevně snažil investovat minimum. Čím více chyb v GF, tím lépe pro WebLogic.

Přišel rok 2014 a náš zákazník nakonec souhlasil s "upgrade". Udělali jsme skok z JEE5+JDK6+GF2.1.1 rovnou na GF4+JDK8. Ukázalo se, že to není až tak snadné díky chybám, které ve zdrojácích zevlovaly už od Glassfishe3, čili často 5 i více let.

Payara

Začal jsem zkoumat licence a možnosti, jak se dostat do SVN. Po dnech a nocích, kdy se exporty po různu zasekávaly a padaly díky vadné integritě dat v SVN Oracle, se mi to nakonec podařilo. Nakonec jsem posílal patche přímo do JIRA ... a odezva ... na přijetí jsem čekal nějaké 3 měsíce.

To mě moc nebavilo a proběhla komunikace s šéfem Glassfishe Rezou Rahmannem - pravda, překvapilo mě už, že se se mnou baví, ale ještě víc mě překvapilo, že mě nasměroval na Steva Millige z C2B2 a Payaru.
Po další komunikaci byla moje oprava přijata během 3 dnů do Payary.

Po zastavení komerční podpory Glassfishe se totiž Steve rozhodl, že jí tudíž začne poskytovat sám. Udělal stejný krok jako já, převedl SVN na GitHub, domluvil se s Rezou, a postupně se k němu přidali další a další lidé z komunity, včetně mě.

Tým Payara a jeho přispěvatelé od té doby opravili spoustu chyb a jen tak mezi řečí pomalu posouvají Payaru dál ke specifikaci JEE8. Objevily se požadavky pro podporu Javy IBM, podporována je i Oracle JDK8 (původně 7), opravy se promítají zpětně do Glassfishe od Oracle a samozřejmě - existuje placená podpora, které bych vytkl jen to, že její ceny jsou typicky "enterprise", leč konkurenti nejsou levnější.

Krom toho žije dál i Embedded Payara a vznikly i další distribuce, většinou osekané o nepotřebnou funkcionalitu.

Nakonec vzniklo i toto zábavné reklamní video:

Placená podpora

Na druhou stranu, za Tomcat podporu taky neplatíte, že? Ale to, co umí Payara rozhodně neumí ;-)

Ve výsledku placená podpora slouží hlavně k čestné podpoře profesionálních týmů, které se o tyto projekty starají. Proto pokud pracujete na projektech pro velké firmy či stát, měli byste podporu platit.
Ve skutečnosti sice nezískáte větší podporu, než máte, leč přispíváte tím k tomu, aby aplikační server, který používáte, mohl být dál vyvíjen a udržován, tj. abyste po dvou letech nezjistili, že projekt prostě skončil a vy jedete na mrtvém koni. Což není tak málo, když si to tak vezmete, ne?

V tuto chvíli to vypadá tak, že C2B2 míří mezi velké firmy. Její vývojáři konzultují chyby ve Weld i EclipseLinku, komunita si vypomáhá, zatímco Oracle si stále něco plácá na svém písečku a o ostatní se moc nestará, ale možná je to jen můj pocit ...

Budoucnost

Kdyby někdo měl pocit, že na to má, tým Payary shání vývojáře. Já tam zatím nejdu, mám svůj boj jinde, který navíc slouží jako výborný obří reálný testovací příklad pro Payaru. A zatím můžu říct, že se všichni lepšíme.

Momentálně Websphere, WebLogic ani WildFly neumí všechno, co umí Payara. Možná umí něco navíc, možná něco umí lépe, ale ne dost na to, aby mělo cenu přecházet. Zvlášť když je Payara možná ještě převálcuje :-)

Řekl bych, že i Oracle na to nakonec nutně přijde a bude muset se k tomu postavit čelem. Těžko může komplikovat práci ostatním skrz licence, spíše bych hádal, že se C2B2 pokusí koupit a zklikvidovat, pokud tato konkurence začne být až moc silná.
Dokud ale bude Glassfish na GitHub, bude velmi těžké jeho "lepší klony" kontrolovat, uzmout zpět, takže tahle cesta Oracle asi nepomůže, ne na dlouho. Glassfish není jediný takový projekt, open source se dá sice všelijak poškodit, ale jen stěží kontrolovat, pokud má pod nohama tak silnou infrastrukturu, která má navíc pozitivní efekt i pro komerční obříky.

Časy se mění ... jedno riziko bych tu ale viděl - a tím je paradoxně TTIP (v odkazu TPP, což je podobná dohoda) a různé formy kontroly internetu. Představte si, že vám vláda zablokuje, omezí nebo zpoplatní přístup na GitHub ... že je to paranoidní nápad? Možná - ale třeba tlak například na omezování síly šifer už tu dávno je.
Mimochodem, na některé weby a videa se nedostanete už teď. BBC blokuje přístup na (některá?) videa mimo UK, Youtube "tají" videa před Němci, atd. - co nevidíme, o tom nevím, tohle jsou jen zkušenosti, na které jsme narazili náhodou s kamarády v zahraničí.
To je ale už úplně jiné téma ...

sobota 17. května 2014

Refaktoring, část II.: Technický dluh

Aneb kapitola (nejen) pro manažery, ekonomy, zkrátka byrokracii, která rozhoduje o investicích, financování, směrování projektů - a taky o tom, kdy se projekt uzavře jako "hotový".
Původně jsem chtěl jít rovnou na zdrojáky, ale událo se něco, co mě přimělo vložit ještě jednu kapitolu. Zjistil jsem totiž, že u nás se o technickém dluhu až zase tak moc nemluví. Nicméně programátorší "guru" o této metafoře mluví už docela dlouho:

Co je to?

Manažeři i zákazníci milují vodopádový model: objednávka, zadání, analýza, zhotovení, akceptace, zaplacení. Nic složitého to přece není, vypadá to triviálně a jednoznačně. Ti zkušenější už ví, že každá ta fáze skýtá mnohá nebezpečí a pasti. Obecně nejednoznačnost a nedotažení každé té fáze - příčinou je obvykle neznalost přesných požadavků a neznalost způsobu výroby na druhé straně.

Zjednodušeně řečeno, technický dluh vzniká vždy, když kdokoliv na projektu odloží něco, o čem dobře ví, že je třeba udělat, ale odloží to - ať už se to týká analýzy, testů, dokumentace, vyčištění kódu.

Jak moc to vadí?

To je různé - asi jako inflace, státní dluh, vaše dluhy; proto se tomu říká technický dluh. Jsou to nedodělky, ale ne jen ty, které vidíte při předvádění aplikace. Ty skryté jsou daleko nebezpečnější. Proč? Nedodělky, které vidí uživatel, viděli všichni během vývoje projektu, a došlo k nějakému konsenzu, že jde o kompromis, se kterým uživatel dokáže žít.
Nedodělky, které ale vidí jen programátoři (pokud je vůbec někdo vidí), mají vlastnost právě té inflace - je to exponenciální funkce. S každou další iterací se umocňuje vliv dluhu, veškerá jeho negativa. Dlouho to nemusí vadit, ale když nad technickým dluhem ztratí vývojový tým kontrolu, už není cesty zpět a vývoj projektu skončí s potupnou ztrátou a obviňováním všech, kteří se na něm podíleli, navzájem.


Exponenciální funkce

Je jasné, že udržet projekt bez dluhů je prakticky nemožné. Vždycky se dá všechno udělat lépe. Na druhou stranu, když dluhy nesplácíte, špatně skončíte. Z vlastní praxe bych to rozdělil na takové tři kategorie (v horším případě fáze) ...

Dluh pod kontrolou

Dobrý stav, který znamená, že projekt má budoucnost a přestože obsahuje pár chyb (někdy i hodně), má smysl v něm pokračovat. Příznaky jsou následující:
  • vývojáři dávají celkem rozumné odhady pracnosti
  • nikdo není nervózní, panují dobré vztahy
  • vývojáři se těší na další úkol
  • většinou se stíhají termíny

Zadlužení

To už je horší stav, ale není nezvladatelný. Nesmí se podcenit - i za cenu oddálení termínu předání další verze je nutné dluh udržet nebo ideálně snížit. S každým dalším nárůstem se situace zhoršuje. Příznaky tohoto stavu jsou takové:
  • vývojáři pracují přesčas, často neplaceně a dobrovolně
  • zpravidla se nestíhají termíny, předání verze se oddaluje i opakovaně
  • často se mění analýza během vývoje
  • množí se požadavky na "až"
  • horší se přesnost odhadů pracnosti - obvykle se podstřelují v toužebné snaze všech stihnout termín
  • tendence přidávat lidi do zpožděného projektu
Management nechápe, proč se dříve termíny stíhaly a teď ne, má tendenci přitlačit, motivovat, ale prakticky dosahuje jen jediného - zvýšení tlaku a stresu, což často končí odchodem zaměstnanců, zpravidla těch nejlepších v první řadě, těch nejhorších potom v řadě druhé. Zůstávají jen bojovníci - pokud se dokážou vzepřít veškeré nepřízni, má projekt ještě naději.

Exekuce se blíží

V tuto chvíli se podívejte opět na ten graf exponenciální funkce. V určitém bodě se dostanete přes hranici, kdy vývoj dalších verzí projektu stojí ohromné zdroje a úsilí a jste ve stavu, kdy je extrémně těžké s tím něco začít dělat.
  • vývojáři často mění své odhady, klidně o dva řády - z hodiny je týden, z týdne 20 minut.
  • jakýkoli termín vyvolává šílený smích vývojářů
  • panuje nervozita a dochází k hádkám a práskání dveřmi
  • manažeři zakazují jakoukoliv údržbu, dovolené, a snaží se do projektu dostat nové lidi - a to jakékoliv
  • neprovádí se analýza, nebo jen povrchně
  • nehledí se na žádná kvalitativní měřítka
Je téměř vyloučeno, abyste se dostali z této fáze zpět. Pokud chcete v budoucích projektech uspět, uvědomte si, co jste zanedbali dříve, podcenili. Není to o tom, že jste měli požadovat vyšší cenu nebo sehnat více lidí.  Vždy potřebujete čas a vždy potřebujete nějakou stabilní kvalitu. Na tom, co děláte dnes, budete stavět zítra.

Udržování dluhu pod kontrolou

Všechno je vlastně docela snadné a pro řadu souvisejících problémů dokonce existují nástroje.

Odhadování pracnosti

To je problematika, na které často stojí váš úspěch - odhadnout, kolik času budete potřebovat na zhotovení něčeho, o čem ještě nemáte "ani páru", je trochu neřešitelný úkol. Existuje na něj řada strategií a doporučení, ale vždy ke kvalifikovanému odhadu potřebujete přehled. Odhad navíc nemůže být definitivní - je to jen odhad, že ...

P: "Devět žen neporodí ani jedno dítě za jeden měsíc. Chápeš?"
M: "Jojo, tohle ví každej, to znám ... Ale Ty jsi chlap!"
P: "Máš pocit, že devět chlapů nějaké dítě porodí?" 

Jak se projevuje technický dluh na odhadu? Představte si, že máte nějakou knihovnu, kterou lehce zanedbáváte - používáte jí ale v aplikaci bez problémů. Přijde ale nový úkol pro aplikaci, při kterém ale zjistíte, že v knihovně je chyba. Také zjistíte, že chybu jste v jiných aplikacích, které na ní už narazily, obešli. Jenže tuto obezličku v nové aplikaci uplatnit nemůžete, protože je v rozporu se zadáním - a navíc jste tehdy nepsali ani testy, takže ani nevíte, co všechno se opravou naopak rozbije.
A tak vám nezbude, než chybu opravit, čímž ale možná rozbijete již hotové aplikace s obezličkou. Tudíž pak budete muset i dopsat testy a opravit i tyto aplikace.

A teď se krátce zamyslete - jaký asi byl původní odhad? Kolikrát ho během opravy změníte? A jaká byla výsledná pracnost? Tím to ale nekončí - opravené aplikace bude možná třeba také distribuovat, takže nám vzniká další pracnost.
Ufff, tohle bolelo. A ještě bude, protože všem musíte vysvětlovat, co se vlastně stalo a proč - a čas běží dál a náklady rostou.

Psaní automatických testů

Automatický test je vlastně další kód, který programátor napíše nejlépe předtím, než začne programovat nějakou funkcionalitu aplikace. Test není součástí aplikace, ale verzuje se spolu se zdrojáky, a moderní programovací jazyky velmi pečlivě zohledňují testovatelnost.
Není žádná výmluva pro nepsaní testů, nikdy. Už dobrých 20 let se považuje za prokázané, že automatické testy vedou k
  • rychlému nalezení chyb nového kódu
  • ujasnění designu a zpětné vazbě analýze dříve, než je aplikace hotová
  • rychlému nalezení chyb, které způsobily opravy na jiném místě
  • konzistentnímu refactoringu (nic se nerozbije)
  • dokumentaci funkcionality (test minimalisticky ukazuje, jak se funkcionalita používá)
Naopak prosby nebo dokonce zákazy manažerů, aby se psaní testů odložilo, protože není čas, končí tak, že
  • dostanou funkcionalitu ještě později
  • druhý den se opravuje oprava dne předchozího, den za dnem
  • nikdo neví, co to vlastně dělá a k čemu to je (brzy ani autor)
  • jakákoliv změna v kódu znamená nutnost manuálního přetestování skoro celé aplikace, protože nikdo neví, co všechno změna ovlivnila

Refactoring

Refactoring se přímo zaměřuje na snižování technického dluhu. Obvykle je dobré začít psaním testů, dopisováním //FIXME a //TODO, případně komentářů, kam si zapíšete své objevy proč a co se v tom daném místě děje, co je na tom špatně, jak by to mělo být správně. Tyto komentáře neslouží k tomu, aby v kódu zůstaly, ale abyste se při své analýze neztratili.
Musíte postupovat opatrně, protože se pohybujete na "minovém poli" (proto kód chcete přece refaktorovat), a krok vedle může znamenat, že své úpravy zahodíte (dokud je ještě čas).

Refactoring předně slouží k tomu, aby byl kód čitelný, měl jasné odpovědnosti a funkcionalitu, choval se předvídatelně a funkcionalita byla vždy k nalezení tam, kde jí člověk hledá. Potom se na kódu teprve dá stavět něco dalšího, kde nebudete muset vymýšlet žádné obezličky.

Refactoring nikdy nekončí - ke každému kódu se po čase musíte iterativně vracet, protože jak se rozvíjí aplikace, je občas třeba změnit trochu i uspořádání kódu, sloučit věci, které se původně zdály rozdílné, ale nejsou, rozdělit věci, které původně dělaly téměř totéž, ale už dávno to není pravda, atd.
Ač se to některým lidem zdá pořád neuvěřitelné, nečitelnost kódu, velké množství duplicit a slabé pokrytí testy mají extrémní vliv na jakýkoliv budoucí rozvoj, daleko větší než sebekomplikovanější zadání.

... a odkládání

Pokud se údržba zanedbává, problémy na sebe nenechají dlouho čekat:
  • náklady na rozvoj aplikace jsou čím dál vyšší, neúměrně požadavkům zákazníka
  • opravené chyby uživatel opět hlásí jako neopravené (našel je i jinde)
  • aplikace se chová nekonzistentně (a uživatel jí nenávidí)
  • aplikace potřebuje více paměti a je pomalá
  • vývojáři trvá velmi dlouho, než zjistí, co má vlastně dělat, těžko se orientuje
  • n testů téže věci a podobná věc není otestovaná vůbec
  • nepřehledná dokumentace, nepřehledné testy, nepřehledná aplikace
  • vývojáři nenávidí aplikaci a po čase odchází jinam (nepodceňovat!)
Často se ale zapomíná také na to, že jsme jen lidé a zapomínáme. O týden odložená údržba už znamená, že se v ošklivém kódu přestává orientovat i jeho autor, a nejen rozvoj aplikace, ale i její údržba stojí více, je namáhavější a také při ní pravděpodobně vznikne více chyb.Je to podobné jako s úvěry - u některých můžete odložit několik splátek, ale pak je bude mnohem těžší dohnat. Možná to už nezvládnete ...

Nástroje

Co se týče sledování odhadu technického dluhu u nás používáme SonarQube; V této aplikaci je i řada dalších metrik kvality software a dá se říct, že je to jediná aplikace, kterou znám, která umí zobrazovat i historii různých hodnocení projektů a dá se i zhruba použít k porovnávání kvality. Podotýkám, zhruba, protože žádný software nemůže posoudit to, jak vaše aplikace plní požadavky uživatele a zákazníka.

No a pokud jde o nástroje pro vývojáře a tvorbu automatických testů, refactoring a vývoj obecně, ti už "ty svoje" nástroje určitě dobře znají ;-)